Dissecting a wavy hatch: sine, refraction and serendipity | Codrops

Dissecting a wavy hatch: sine, refraction and serendipity | Codrops

3 minutes, 47 seconds Read

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:

  • requestAnimationFrame for 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

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *