Mesh Distortion AI
A lattice of 13 × 13 points linked by thin lines, drawn on a canvas, that ripples gently on its own; within 100 px of the mouse, points are pulled toward it by up to 20 px and the net pinches under the cursor, lines included. A hue that slides from blue to violet, a canvas sharp on Retina, a loop that stops off screen, a still net that lights up under the mouse when the user reduces motion. A net of points, not a mesh gradient; no artificial intelligence.
Updated
This effect is part of Effect.Labs — 840 vanilla effects, some free, some premium. Liquid has 31 effects, including 6 free. Explore the category →
3 usage examples



How it works
The .fx-0420-root frame is 300 px tall and full width, and the canvas fits it (display: block, 100% × 100%); internal size = displayed size × devicePixelRatio, drawing in CSS pixels through setTransform (measured: 1280 × 600 for 640 × 300 at density 2). The lattice covers the whole canvas: 12 intervals each way, about 53 × 25 px cells on 640 × 300, with edge points sitting on the edge. A ResizeObserver follows the frame, and so does the lattice.
Each frame is drawn in two passes. First, each point's position: its place in the grid, plus a ±3 px ripple (3 sin(a + 0.5 × column) horizontally, 3 cos(a + 0.5 × row) vertically, a moving by 0.015 per frame, a cycle of about 7 s at 60 frames per second), plus, if the mouse is within 100 px, a shift toward it of 20 × (1 − distance / 100) px. Then the 1 px lines linking each distorted point to its right and lower neighbors, distorted as well, and finally the 2 px radius points on top. Points close to the cursor gather on it: seen from above, the net dips as if pressed. The hue follows 240 + 30 sin(a + 0.3 × (column + row)): a violet band crosses the blue diagonally.
The mouse is read through mousemove on the container. Until it has entered, and as soon as it leaves (mouseleave), its position is stored far away from the frame: no point is pulled and the net returns to its ripple alone. The canvas is cleared on every frame, so its background is the root's, or your page's if the root is transparent. The requestAnimationFrame loop stops when an IntersectionObserver sees the canvas leave the screen (measured: 0 frames per second off screen against 120 on screen) and resumes when it comes back.
Accessibility
- Reduced motion: under
prefers-reduced-motion: reduce, the net no longer ripples or gets pulled; it is redrawn only when the mouse moves, and points within 100 px lighten and turn pink, their lines more opaque, without moving. Measured in Chromium 151, Firefox 153 and WebKit 26: 0% of pixels changed at rest, the area under the mouse lights up as it passes. - Keyboard and screen readers: the net only reacts to the mouse and carries no information. If the distortion illustrates a point (a net's strength, a fabric's give), say it in text next to it, as in the examples.
- Touch: with no hover, a tap sends a single
mousemove: the net pinches at the touched point and stays there until the next tap, while the ripple goes on. To follow a finger, see the FAQ. - Contrast: lines are at 15% opacity, mere mood strokes on a dark background; the points carry the shape. On a light or busy backdrop, raise the line opacity (see Customize).
Browser compatibility
2D canvas + 2.9 KB of JavaScript, no dependency. Measured in Chromium 151, Firefox 153 and WebKit 26: same ripple, same pinch under the mouse, return when it leaves, internal size doubled at density 2, stable height after 20 resizes, loop stopped off screen, still net under reduced motion. Measured cost: 0.2 ms of JavaScript per frame in Chromium, about 1 ms in Firefox. ResizeObserver exists since Chrome 64, Firefox 69 and Safari 13.1.
Without JavaScript, the frame stays bluish black, with no net. Without IntersectionObserver, the loop also runs off screen; without ResizeObserver, the lattice follows window resizing only.
The code
HTML structure to paste into your page (the CSS + JS come with your purchase):
<div class="fx-0420-root">
<div class="ai-mesh-container">
<canvas class="ai-mesh-canvas" width="393" height="280"></canvas>
</div>
</div>
One-time payment, no subscription — the code is yours forever
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
Density (JS — const h = 12) |
12 intervals: 13 × 13 = 169 points | 20 gives 441 points and a fine net; 8, 81 points and a coarse mesh. Cost follows the number of points. |
Radius and strength (JS — m = 100 and 20 * (1 - g / m)) |
100 px, up to 20 px of pull | 160 px and 30 px for a wide, deep dip; 60 px and 10 px for a fingertip press. |
Direction (JS — u += r / g * t, M += d / g * t) |
pull toward the mouse | Replace both += with -=: points flee the cursor and the net bulges around it. |
Ripple (JS — 3 * Math.sin(a + .5 * n), 3 * Math.cos(a + .5 * t) and a += .015) |
±3 px, a cycle of about 7 s | Put 0 instead of the two 3s for a perfectly still net at rest; 0.03 for a doubles the speed. |
Colors (JS — 240 + 30 * Math.sin(…), hsla(${k}, 70%, ${L}%, 0.8)) |
hue 210 to 270, saturation 70%, lightness 60% | Hue 34 and lightness 78% give sand; saturation 0 and lightness 94% a white net. Lines have their own hsla(${k}, 60%, 50%, ${A}). |
Line opacity (JS — A = .15) |
15% | 0.4 to 0.5 makes the net readable on a light or busy backdrop, as in all three examples. |
Height and background (CSS — .fx-0420-root { height: 300px; background: #0a0a0f }) |
300 px, bluish black | The canvas is cleared on every frame: background: transparent lays the net over your backdrop. |
FAQ
pointermove and pointerleave instead of mousemove and mouseleave (same clientX / clientY coordinates), and add touch-action: none on the container: the finger pinches the net as it slides, but the page no longer scrolls while the finger is on it. So keep a frame that does not cover the whole screen.