Every creative studio has its strange internal rituals.
Our? We call it a challenge “12 pens in 12 months” – one experiment every month, no rules, no customers, just play.
Somewhere between caffeine and chaos, one of our developers created this: an undulating, hypnotic, jelly-like motion effect that became an instant team favorite. We didn’t plan on making anything “serious”, but people started asking how it works, and here we are – writing about it on Codrops!
The Concept
The idea started simple:
What if we could create movement that felt organic – not mechanical, not linear, but something that flows?
We wanted that ‘liquid between two worlds’ feeling – a movement that seems to breathe, stretch and relax.
At its core it is a mix of:
- fragment shaders (to generate geometric wave-like cells)
- math-driven distortion (sine waves, ripples and refraction)
- And requestAnimationFrame for smooth, continuous GPU updates.
function draw(tms) {
const t = tms * 0.001;
// organic motion: a sine wave that gently distorts the grid size over time
const wave = Math.sin(p.x * 0.01 + p.y * 0.015 + t) * 0.25;
const localCell = cell * (1.0 + wave * 0.2);
// ripple effect triggered by user interaction (click)
float ripple = sin(R * 0.06 - dt * 6.0) * env;
// blending the glass-like refraction with the base image
vec3 col = mix(base, glass, inside);
gl_FragColor = vec4(col, 1.0);
}
requestAnimationFrame(draw);How it works
The magic trick here is in the way we recompute the wave path every frame.
Instead of animating DOM or CSS properties, we dynamically rebuild each pixel in the shader – frame by frame.
Each cell behaves like a living surface: pulsating, refracting and undulating over time.
Imagine drawing a line with a rubber band: each anchor point follows the previous one, with a little delay and overshoot. This postponement is wonderful for us sweet, alive movement.
For performance reasons we use:
requestAnimationFramefor smooth updates.- GPU-driven mathematics — sine, refraction, ripple calculated directly in the hatch.
- requestAnimationFrame to synchronize GPU frames with the browser refresh rate.
// find the nearest cell center based on the selected shape
void nearestCenter(int shape, vec2 p, float cell, out vec2 c, out vec2 lp) {
if (shape == 0) {
vec2 qr = hex_pixel_to_axial(p, cell);
vec2 qrr = hex_axial_round(qr);
c = hex_axial_to_pixel(qrr, cell);
lp = p - c;
} else {
vec2 g = floor(p / cell + 0.5);
c = g * cell;
lp = p - c;
}
}Presets and variations
Once the core system was up and running, we couldn’t stop tweaking it.
Instead of fixed presets, we built a simple control panel: four sliders that directly control the shader parameters:
- Cell size (
uCell) — density of the grid - Amplitude (
uAmp) — refractive power - Chromatic shift (
uChrom) — amount of color separation - Speed (
uSpeed) — how fast the wave evolves
Changing just one of these immediately changes the mood of the movement from soft and smooth to sharp and energetic.
At each frame, the app reads the live values from the sliders and sends them directly to the shader –
so every small change immediately ripples through the entire surface.
function draw(tms) {
const t = tms * 0.001;
// live parameters from the UI → shader
gl.uniform1f(uCell, parseFloat(cellInp.value));
gl.uniform1f(uAmp, parseFloat(ampInp.value));
gl.uniform1f(uChrom, parseFloat(chromInp.value));
gl.uniform1f(uSpeed, parseFloat(speedInp.value));
gl.uniform1f(uTime, t);
// update the video texture (if active)
if (videoReady) {
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, videoEl);
}
// render + next frame
gl.drawArrays(gl.TRIANGLES, 0, 6);
requestAnimationFrame(draw);
}Challenges along the way
Of course, it wasn’t all plain sailing (pun intended).
At some point the wave became complete chaos: spikes, flickers, everything fell apart.
It turned out that our smoothing logic was off by one pixel (literally).
Another funny bug: when testing on high refresh monitors, the movement looked off too smooth – as if it lost its texture. So we had to add a touch of imperfection to bring back the ‘handmade’ atmosphere.
Try it yourself
The whole setup is open and easy to remix — just fork it and start playing with the parameters.
Try changing the following:
- the number of points
- the relaxation curves
- the color gradients
We’d love to see what kind of waves you’re making – tag us or send your remix!
Final thoughts
Sometimes the best ideas come when you don’t try too hard.
This was intended as a quick internal experiment, but it turned into something strangely satisfying and visually rich.
We hope you enjoy it as much as we enjoyed breaking (and repairing) it.
Created with ❤️ by Blacklead Studio as part of our 12 pens in 12 months creative challenge.
#Dissecting #wavy #hatch #sine #refraction #serendipity #Codrops


