Advertisement

WebGL Fluid Smoke Button

| by Vladimir | 2 min read | code by Tim Wilson
Advanced

Tech & Dependencies

JavaScript HTML CSS
fragment-canvas Tailwind CSS

Features

  • Procedural Generation
  • Fragment Shader
  • Fluid Animation
  • Noise Algorithms

Browser Support

Chrome 90+ Edge 90+ Firefox 90+ Safari 15+

Core

This WebGL Fluid Smoke Button elevates standard UI interactions by embedding a real-time generative shader directly into the interface. Instead of using a heavy video file, it calculates complex fluid dynamics on the fly using Fractional Brownian Motion (fBM). The result is a mesmerizing, never-repeating smoky background that reacts to hover states with a smooth scale and rotation effect, styled effortlessly via Tailwind CSS.

Core Technique

The visual magic relies entirely on a GLSL Fragment Shader. The shader implements a technique called Domain Warping.

  1. FBM Noise: It layers multiple frequencies of noise (noise function) to create the cloud-like texture.
  2. Domain Warping: Instead of just drawing the noise, the code uses the noise value to distort the coordinate system of another noise calculation. This fbm(uv + r) logic creates the swirling, fluid motion characteristic of smoke or marble.
  3. Wrapper Library: The JavaScript uses a lightweight helper, FragmentCanvas, to handle the WebGL boilerplate (creating the context, compiling shaders, and updating the iTime uniform).

Customization

To change the aesthetics, you need to modify the GLSL code inside the fragmentShader string. The colors are defined using mix() functions near the end of the main() function.

void main(void) {
    // ... calculation of 'f' ...

    // Base dark color
    vec3 color = mix(
        vec3(0.0, 0.0, 0.0), 
        vec3(0.0, 1.0, 0.5), // Change to Green
        clamp((f * f) * 6.0, 0.0, 5.0)
    );

    // Secondary highlight
    color = mix(
        color,
        vec3(1.0, 0.2, 0.0), // Change Magenta to Orange
        clamp(length(q) * length(q), 0.0, 1.0)
    );
    
    // ...
}

To adjust the speed of the animation, modify the uniforms object in the JavaScript:

new FragmentCanvas(canvas, {
    fragmentShader,
    uniforms: {
        // Divide by a smaller number to speed up
        offset: (gl, location, time) =>
            gl.uniform1f(location, Math.cos(time / 500) * 1.0) 
    }
});

Tips

1. Performance Impact: Running a complex fragment shader (calculating noise per pixel) consumes GPU resources. While fine for a single “Hero” button, avoid using this on list items or grid elements (e.g., 20+ buttons at once), as it will significantly drain battery on mobile devices.

2. Handling Resize: The snippet includes a basic resize handler. In a production environment (especially React/Vue), ensure you disconnect the resize listener or destroy the WebGL context when the component unmounts to prevent memory leaks.

Advertisement