Fluid background animation on mouse move

Does anyone know any tutorial for re-creating this background effect from this website: https://chriskalafatis.com/

As we move the mouse cursor, there is a fluid trail that follows.

import React, { Component, createRef } from 'react';
import * as THREE from 'three'; 

class BackgroundComponent extends Component {
  constructor(props) {
    super(props);
    this.containerRef = createRef()
    this.state ={
        time: 0
    }
  }

  componentDidMount() {
    this.container = this.containerRef.current;
    this.width = this.container.offsetWidth;
    this.height = this.container.offsetHeight;

    this.renderer = new THREE.WebGLRenderer({
      alpha: true,
      antialias: true,
    });

    this.dispScene = new THREE.Scene();
    this.scene = new THREE.Scene();
    this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    this.renderer.setSize(this.width, this.height);
    this.container.appendChild(this.renderer.domElement);
    this.camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 100, 2000);
    this.camera.position.set(0, 0, 600);
    this.manager = new THREE.LoadingManager();
    this.loader = new THREE.Curve(this.manager);
    this.time = 0;
    this.mouse = new THREE.Vector2();
    this.prevMouse = new THREE.Vector2();
    this.total = 100;
    this.currentSmoke = 0;
    this.s = 0;
    this.renderTex = new THREE.RenderTarget(this.width, this.height, {
      minFilter: THREE.LinearFilter,
      magFilter: THREE.LinearFilter,
      format: THREE.RGBAFormat,
    });

    this.addBg();
    this.addSmoke();
    this.animate();
  }

  addSmoke() {
    this.smoke = true;
    // const t = this.data.data.media;
    // const e = window.TEXTURES[t][0];
    const i = new THREE.Vector2(55, 57); // Create a Vector2 with appropriate dimensions

    this.dispM = [];
    for (let t = 0; t < this.total; t++) {
      let smokeParticle = new THREE.Mesh(
        new THREE.BufferGeometry(i.x, i.y), // Use PlaneBufferGeometry
        new THREE.MeshBasicMaterial({
        //   map: e,
          transparent: true,
          blending: THREE.NormalBlending, // Use appropriate blending mode
          depthTest: false,
          depthWrite: false,
        })
      );

      smokeParticle.visible = false;
      this.dispM.push(smokeParticle);
      this.dispScene.add(smokeParticle);
    }
  }

  addBg() {
     // Create a shader material for the background
     this.material = new THREE.ShaderMaterial({
        extensions: {
          derivatives: "#extension GL_OES_standard_derivatives : enable",
        },
        uniforms: {
          time: {
            value: 0,
          },
          tex: {
            value: null,
          },
          t: {
            value: 0,
          },
          t2: {
            value: 0,
          },
          pt: {
            value: 0,
          },
          st: {
            value: 0,
          },
          ps: {
            value: new THREE.Vector2(window.innerWidth, window.innerHeight),
          },
          r: {
            value: 0,
          },
          fc: {
            value: 0,
          },
          vc1: {
            value: new THREE.Vector2(0.5, 0.5),
          },
          vc2: {
            value: new THREE.Vector2(0, 1),
          },
          vc3: {
            value: new THREE.Vector2(1, 0),
          },
          c1: {
            value: new THREE.Vector4(0, 0, 0, 1),
          },
          c2: {
            value: new THREE.Vector4(0.3, 0.3, 0.3, 0.95),
          },
        },
        transparent: true,
        vertexShader: `
          #define GLSLIFY 1
          uniform float t;
          uniform float t2;
          uniform float time;
          uniform float pt;
          varying vec2 vUv;
          
          float ea1(float x) {
            return x < 0.5 ? 16. * x * x * x * x * x : 1. - pow(-2. * x + 2., 5.) / 2.;
          }
          
          float ea2(float x) {
            return x < 0.5 ? 8. * x * x * x * x : 1. - pow(-2. * x + 2., 4.) / 2.;
          }
          
          float pi = 3.14159265359;
          
          void main() {
            vUv = uv;
            vec3 pos = position;
            pos.z -= 85.;
            vec2 ct = vec2(0.5);
            float p = ea1(t);
            float p2 = ea2(t2);
            float np = min(2. * p2, 2. * (1. - p2));
            pos.y -= .15 * sin(uv.x * pi + 0.25) * np;
            pos.y += 1. * (1. - p);
            float nPt = min(2. * pt, 2. * (1. - pt));
            // pos.x *= 1. -(0.05 * (pt));
            float dist = distance(vUv, ct);
            float md = length(ct);
            float nd = dist / md;
            float b = nd * 150.;
            float c = -nd * 150.;
            float fm = mix(c, b, pt);
            // pos.z -= fm * nPt;
            float dt = distance(vec2(uv), vec2(0.5)) * 1.25;
            pos.z -= dt * 85. * pt;
            pos.y *= 1. + (0.11 * pt);
            gl_Position = projectionMatrix * modelViewMatrix * vec4( pos, 1.0 );
          }
        `,
        fragmentShader: `
          #define GLSLIFY 1
          #define PI 3.14159265359
          #define PI2 6.28318530718
          uniform float time;
          uniform float pt;
          uniform float st;
          uniform sampler2D tex;
          uniform vec2 ps;
          uniform float r;
          uniform float fc;
          uniform vec2 vc1;
          uniform vec2 vc2;
          uniform vec2 vc3;
          uniform vec4 c1;
          uniform vec4 c2;
          varying vec2 vUv;
          
          float rand(vec2 st){
            return fract(sin(dot(st.xy, vec2(12.9898,78.233))) * 43758.5453123);
          }
          
          float ns(vec2 p){
            vec2 ip = floor(p);
            vec2 u = fract(p);
            u = u*u*(3.0-2.0*u);
            
            float res = mix(
              mix(rand(ip),rand(ip+vec2(1.0,0.0)),u.x),
              mix(rand(ip+vec2(0.0,1.0)),rand(ip+vec2(1.0,1.0)),u.x),
              u.y
            );
            return res*res;
          }
          
          float ub( vec2 p, vec2 b, float r ){
            return length(max(abs(p)-b+r,0.0))-r;
          }
          
          vec4 la(vec4 frg, vec4 bc) {
            return frg * frg.a + bc * (1.0 - frg.a);
          }
          
          float aastep(float threshold, float value) {
            float afwidth = length(vec2(dFdx(value), dFdy(value))) * 0.70710678118654757;
            return smoothstep(threshold-afwidth, threshold+afwidth, value);
          }
          
          float sw(vec2 pt, vec2 center, float radius, float line_width, float edge_thickness, float side){
            vec2 d = pt - center;
            float theta = time * .25 * side;
            vec2 p = vec2(cos(theta), -sin(theta))*radius;
            float h = clamp( dot(d,p)/dot(p,p), 0.0, 1.0 );
            //float h = dot(d,p)/dot(p,p);
            float l = length(d - p*h);
          
            float gradient = 0.0;
            const float gradient_angle = PI * .25;
          
            if (length(d)<radius){
              float angle = mod(theta + atan(d.y, d.x), PI2);
              gradient = clamp(gradient_angle - angle, 0.0, gradient_angle)/gradient_angle * 0.5;
            }
          
            return gradient + 1.0 - smoothstep(line_width, line_width+edge_thickness, l);
          }
          
          void main(){
            vec2 m = vUv;
            vec2 m2 = vUv;
          
            vec4 n = texture2D(tex, vUv);
            float z = n.r * 2. * PI;
            vec2 dr = vec2(sin(z));
            vec2 uv = vUv + dr * n.r * 0.1;
            float d1 = distance(uv, vc1);
            float d2 = distance(uv, vc2);
            float d3 = distance(uv, vc3);
            float gr = mix(-0.2, 0.2, rand(uv + sin(time)));
            vec2 mv = vec2(time *0.05, time * -0.05);
            float f = ns((uv * d1 * 2.) + mv);
            f += ns((uv * d2 * 2.5) + vec2(time * -0.075, time * 0.05));
            f += gr;
            f = smoothstep(0., 2., f);
            // f = fract(f);
            float mx = smoothstep(0., 0.1, f) - smoothstep(0.5, 1., f);
            vec4 color = mix(c1, c2, f);
            float nPt = min(2. * pt, 2. * (1. - pt));
            float u = .3 * sin(m.x * PI + 0.25) * nPt;
            float d = -.3 * sin(m.x * PI + 0.25) * nPt;
            m.y -= mix(u, d, st);
            float ft = step(m.y, pt);
            vec2 rs = ps * (0.5 - vec2(fc * 0.35, fc));
            vec2 rs2 = ps * (0.5 - vec2(fc * 0.35, fc));
            float ra = r;
            vec2 yl = vUv;
            yl.y += 0.002;
            yl.x += 0.00125;
            float cr = ub(((vUv - vec2(fc * 0.35, fc * 0.75)) * ps) - rs, rs, ra);
            float cr2 = ub(((yl - vec2(fc * 0.35, fc * 0.75)) * vec2(ps.x * 0.99725, ps.y * 0.995)) - rs2, rs2, ra);
            cr = clamp(cr, 0.0, 1.);
            cr2 = clamp(cr2, 0.0, 1.);
            vec4 br = vec4(0.,0.,0.,0.);
            vec4 s1 =  mix(color, vec4(0.,0.,0., 1.), ft);
            vec3 brcl = vec3(77. / 255.);
            brcl += sw(yl, vec2(0.5), 1.5, 0.00003,0.00001, 1.) * vec3(85./255.);
            vec4 final = mix(s1, la(mix(vec4(brcl,1.), vec4(0.), cr2),vec4(0.,0.,0.,1.)), cr);
          
            float alpha = 1.;
            float cut = 0.001;
            alpha *= aastep(cut, m2.x);
            alpha *= 1. - aastep(1. - cut, m2.x);
            alpha *= aastep(cut, m2.y);
            alpha *= 1. - aastep(1. - cut, m2.y);
          
            gl_FragColor =  vec4(final.rgb, alpha);
          }
        `,
      });
  
      // Create a geometry for the background
      this.geometry = new THREE.PlaneGeometry(this.width, this.height, 30, 30);
  
    // Create a mesh using the geometry and material
    this.mesh = new THREE.Mesh(this.geometry, this.material);

    // Scale the mesh to match the window size
    this.mesh.scale.set(window.innerWidth, window.innerHeight, 1);

    // Add the mesh to the scene
    this.scene = new THREE.Scene();
    this.scene.add(this.mesh);
  }

  animate = () => {
    requestAnimationFrame(this.animate);
    // Update your animation here
    // this.material.uniforms.time.value += 0.01; // Example animation
    this.renderer.render(this.scene, this.camera);
  };

  componentWillUnmount() {
    cancelAnimationFrame(this.animate)
  }

  
  render() {
    return (
      <div ref={this.containerRef}>
   
      </div>
    );
  }
}

export default BackgroundComponent;

This is what I have come up with but I am not anywhere near to what I am trying to achieve.

1 Like

Hi, I was also curious about how this effect works, so I spent some time reverse-engineering the background animation from the site.

I created a minimal reproduction of this effect and published it here:

GitHub repo:

The demo contains just two files:

  • index.html

  • noise.jpeg

The noise texture is used inside the shader like this:

const noiseTex = new THREE.TextureLoader().load("noise.jpeg");

When the mouse moves, the script spawns smoke particles and updates shader uniforms, which creates the fluid distortion trail.

This repo is a simple educational example demonstrating how the effect can be implemented using:

  • Three.js

  • WebGL / GLSL shaders

  • Noise-based UV distortion

  • Mouse-driven particle spawning

Disclaimer

This project is an independent recreation of a visual effect originally seen on

https://chriskalafatis.com.

It was created for educational and experimentation purposes only to demonstrate how similar effects can be implemented using WebGL and shaders.

All credit for the original design and concept goes to the creator of the website.

The noise.jpeg texture included in this example was downloaded from the original site. Please respect the creator’s work and consider using your own texture if you reuse this project.

The implementation in this repository was recreated from scratch for learning purposes.

Hope this helps anyone trying to understand how the effect works.