@rippleflow/water-distortion
v0.1.4
Published
A responsive React SVG-filter water refraction layer for portfolio-style interfaces.
Maintainers
Readme
RippleFlow Water Distortion
A React + Vite + TypeScript package for cursor-driven water on portfolio-style interfaces. RippleFlow now uses a low-resolution heightfield simulation: pointer movement stamps local trough/ridge wakes, the wave equation propagates them outward, and a WebGL material turns the heightfield into refraction, specular highlights, crests, and experimental ripple-driven caustic light.
Foreground content stays sharp and interactive. Background content can be sampled directly by WebGL in texture / canvas mode for the strongest refraction, or rendered as live DOM in dom mode.
Install
npm install @rippleflow/water-distortionReact and React DOM are peer dependencies:
npm install react react-domUsage
import { WaterDistortion } from "@rippleflow/water-distortion";
export function Hero() {
return (
<WaterDistortion
mode="texture"
wakeStrength={0.54}
interactionRadius={25}
troughStrength={0.92}
ridgeStrength={0.46}
ridgeOffset={10}
wakeLength={84}
velocityScale={1}
velocityClamp={1.35}
cursorSmoothing={0.2}
segmentSpacing={18}
damping={0.98}
waveSpeed={0.4}
normalScale={20}
refractionStrength={0.7}
specularIntensity={1}
crestIntensity={0.2}
causticIntensity={0}
simulationResolution={192}
texture="/work-collage.jpg"
underlay={
<img
src="/work-collage.jpg"
alt=""
style={{ width: "100%", height: "100%", objectFit: "cover" }}
/>
}
style={{ minHeight: 620 }}
>
<section>
<h1>Portfolio title stays crisp</h1>
<a href="/work">View work</a>
</section>
</WaterDistortion>
);
}For true shader refraction, provide a texture source:
<WaterDistortion
mode="texture"
texture="/hero-background.jpg"
wakeStrength={0.8}
underlay={<img src="/hero-background.jpg" alt="" />}
>
<HeroCopy />
</WaterDistortion>WaterLayer is exported as an alias of WaterDistortion.
React API
| Prop | Type | Default | Notes |
| --- | --- | --- | --- |
| underlay | ReactNode | undefined | Live DOM/background content behind the water. Recommended in dom mode and useful as a fallback in texture modes. |
| children | ReactNode | undefined | Foreground content rendered above the water. |
| foreground | ReactNode | undefined | Explicit foreground prop; takes precedence over children. |
| mode | "dom" \| "texture" \| "canvas" | "texture" | Chooses live-DOM overlay mode or true WebGL background sampling. |
| texture | TexImageSource \| string | undefined | Image/canvas/video source sampled by WebGL in texture or canvas mode. URL strings are loaded with crossOrigin="anonymous". |
| interactionMode | "move" \| "click" \| "drag" \| "event" | "move" | Chooses pointer-movement wakes, one ripple per pointer down, press ripples plus held-drag wakes, or programmatic ripples only. |
| runInBackground | boolean | false | Continues stepping while hidden or unfocused and catches up elapsed time when the browser throttles background callbacks. |
| pauseKey | string | undefined | Toggles simulation pausing when this KeyboardEvent.code is pressed, such as Space or KeyP. Keyboard input in interactive controls is ignored. |
| wakeStrength | number | 0.54 | Overall pointer wake strength. Slow drags stay subtle; fast movement is clamped before it reaches the shader. |
| rippleStrength | number | undefined | Compatibility alias for wakeStrength. |
| strength | number | undefined | Legacy alias mapped to wakeStrength as strength / 48. |
| interactionRadius | number | 25 | Width in CSS pixels of the moving virtual finger/body that parts the water. |
| troughStrength | number | 0.92 | Strength of the narrow negative trough along the pointer path. |
| ridgeStrength | number | 0.46 | Strength of the paired side ridges beside the trough. |
| ridgeOffset | number | 10 | Side-ridge offset from the path centerline in CSS pixels. |
| wakeLength | number | 84 | Base trailing wake length in CSS pixels. Fast movement scales it longer. |
| velocityScale | number | 1 | Multiplier applied before pointer speed is clamped. |
| velocityClamp | number | 1.35 | Pointer speed in CSS pixels per millisecond that maps to full wake strength. |
| cursorSmoothing | number | 0.2 | Low-pass smoothing for the virtual finger position and speed. |
| segmentSpacing | number | 18 | Maximum CSS-pixel spacing before long pointer moves are subdivided into connected wake segments. |
| damping | number | 0.98 | Per-step velocity damping. Higher values let waves travel longer. |
| dissipation | number | undefined | Legacy alias for damping. |
| waveSpeed | number | 0.4 | Stiffness of the damped wave equation. |
| normalScale | number | 20 | Height-gradient scale used to compute visible water normals. |
| refractionStrength | number | 0.7 | Amount of normal-driven texture displacement in texture/canvas modes. |
| specularIntensity | number | 1 | Strength of fixed-light specular highlights. |
| crestIntensity | number | 0.2 | Visibility of height-gradient crests and troughs. |
| causticIntensity | number | 0 | Experimental ripple-driven caustic light that fades when the simulated surface is calm. |
| simulationResolution | number | 192 | Longest side of the simulation texture. Clamped to 64...384. |
| idleTimeout | number | 900 | Minimum active time after input before the renderer begins checking whether ripples have faded enough to sleep. |
| className | string | undefined | Applied to the container. |
| style | CSSProperties | undefined | Merged onto the container. The component enforces relative positioning, hidden overflow, and isolation. |
| reducedMotion | "respect" \| "disable" \| "ignore" | "respect" | respect disables wave input when prefers-reduced-motion: reduce matches. |
Programmatic ripples use normalized container coordinates through the component ref:
import { useEffect, useRef } from "react";
import {
WaterDistortion,
type WaterDistortionHandle
} from "@rippleflow/water-distortion";
function EventDrivenWater() {
const waterRef = useRef<WaterDistortionHandle>(null);
useEffect(() => {
waterRef.current?.triggerRipple({ x: 0.5, y: 0.5 });
}, []);
return <WaterDistortion ref={waterRef} interactionMode="event" />;
}The older blur, maxTrailPoints, maxStamps, and trailSpacing props remain accepted for source compatibility, but the heightfield renderer does not use the old trail-stamp engine.
Modes
dom
Use dom when the background is arbitrary live React/DOM content. The DOM underlay is rendered normally, foreground content stays sharp, and the WebGL canvas draws transparent water highlights derived from the heightfield.
Browsers do not provide an efficient, general API for sampling arbitrary live DOM into WebGL every frame. RippleFlow therefore does not pretend to perform perfect DOM refraction. The visible water material comes from heightfield normals, specular, crest, and caustic lighting, so ripples remain readable even when the DOM itself is not displaced.
texture / canvas
Use texture or canvas when you can provide an image, video, canvas, or URL that WebGL can sample. This path gives the best visual quality because the shader offsets the sampled background with normal.xy from the water heightfield.
The canvas remains transparent when no texture is supplied, while a URL is loading, or if loading or WebGL upload fails, allowing the underlay to remain visible. A successful URL load automatically requests a fresh render.
How It Works
- A WebGL renderer maintains two ping-pong floating-point simulation textures.
- Each texel stores height and velocity.
- Every fixed step samples neighboring heights, accelerates velocity toward the neighbor average, applies damping, and integrates height.
- Pointer input uses
pointermoveplusgetCoalescedEvents()where available, then low-pass filters samples into a virtual cursor. - Pointer movement is stamped as continuous SDF/capsule wake segments, not repeated timed circles.
- Each segment uses a directional profile: center trough, paired side ridges, a trailing wake mask, and subtle behind-only wavelets.
- The material pass computes normals from height gradients, then combines refraction, specular highlights, crest/trough visibility, and projected caustic light that fades with ripple activity.
- The animation loop runs while input is active, then sleeps after ripples measure as visually quiet.
Performance Notes
- The simulation resolution defaults to
192, while the visible canvas still matches the container size. - Keep
simulationResolutionaround128...256for most portfolio hero sections. - The renderer uses refs and an imperative animation loop; React state is not updated per frame.
ResizeObserverkeeps the canvas responsive.IntersectionObserveranddocument.visibilityStatepause offscreen or hidden work when available.toDataURL()is not used in the primary render path.- Dynamic canvas/video texture sources may be uploaded each rendered frame in
canvas/texturemode. prefers-reduced-motionis respected by default.
Browser Notes
The primary path uses WebGL2 with floating-point render targets. A WebGL1 path is attempted when float textures and float color buffers are available. Current Chrome, Safari, and Firefox support the WebGL2 path on modern desktop systems; older browsers or restricted GPU environments may fall back to showing only the underlay and foreground.
Useful APIs:
- WebGL2: https://developer.mozilla.org/en-US/docs/Web/API/WebGL2RenderingContext
- PointerEvent.getCoalescedEvents(): https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/getCoalescedEvents
- ResizeObserver: https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver
- IntersectionObserver: https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver
Demo
npm install
npm run devOpen the local Vite URL and move across the hero. The /playground controls adjust wake shape, smoothing, segment spacing, damping, wave speed, normal scale, refraction, specular, crest/caustic intensity, and simulation resolution.
Quality
npm test
npm run typecheck
npm run build