Flowing Ribbons

A deformable grid of lines that flows and undulates like fabric. Mouse movement and clicks create ripple distortions that spread through the mesh.

Generated using Grepped's AI UI component generator — created from scratch, not pulled from a library.

canvasgridmeshwavesinteractivebackgroundreactiveribbons

Live Preview — customize or regenerate this in the workspace

Loading preview…

Design Intent

A grid of lines deformed by layered sine waves to create a flowing fabric effect, with mouse proximity and click ripples warping the mesh.

CSS

css
* { margin: 0; padding: 0; box-sizing: border-box; }
    html, body { width: 100%; height: 100%; overflow: hidden; background: #F0EEE6; }
    canvas { display: block; width: 100%; height: 100%; }

HTML

html
<canvas id="c"></canvas>

Full Source

html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    html, body { width: 100%; height: 100%; overflow: hidden; background: #F0EEE6; }
    canvas { display: block; width: 100%; height: 100%; }
  </style>
</head>
<body>
  <canvas id="c"></canvas>
  <script>
    var BG = '#F0EEE6';
    var LC = '#777777';
    var SPEED = 0.3;
    var REMOVE_WAVE = true;

    var canvas = document.getElementById('c');
    var time = 0;
    var mouse = { x: 0, y: 0, down: false };
    var disturbances = [];

    function resize() {
      var dpr = window.devicePixelRatio || 1;
      canvas.width = innerWidth * dpr;
      canvas.height = innerHeight * dpr;
      canvas.style.width = innerWidth + 'px';
      canvas.style.height = innerHeight + 'px';
      var ctx = canvas.getContext('2d');
      ctx.setTransform(1,0,0,1,0,0);
      ctx.scale(dpr, dpr);
    }

    function mouseFx(x, y) {
      var d = Math.sqrt((x-mouse.x)*(x-mouse.x)+(y-mouse.y)*(y-mouse.y));
      return Math.max(0, 1-d/200);
    }

    function disturbFx(x, y, now) {
      var tot = 0;
      for (var k=0; k<disturbances.length; k++) {
        var d = disturbances[k];
        var age = now-d.t;
        if (age < 3000) {
          var dx = x-d.x, dy = y-d.y;
          var dist = Math.sqrt(dx*dx+dy*dy);
          var rad = age/3000*400, w = 80;
          if (Math.abs(dist-rad) < w) tot += (1-age/3000)*d.i*(1-Math.abs(dist-rad)/w)*Math.sin((dist-rad)*0.1);
        }
      }
      return tot;
    }

    function deform(x, y, t, progress, now) {
      var mf = mouseFx(x, y);
      var df = disturbFx(x, y, now);
      var w1 = Math.sin(progress*Math.PI*4+t*0.01)*30;
      var w2 = Math.sin(progress*Math.PI*7-t*0.008)*15;
      var h = Math.sin(x*0.02+y*0.015+t*0.005)*10;
      var mw = mf*Math.sin(t*0.02+progress*Math.PI*2)*20;
      var dw = df*Math.sin(t*0.015+progress*Math.PI*3)*25;
      return { ox: w1+h+mw+dw, oy: w2+mw*0.5+dw*0.7 };
    }

    function draw() {
      var ctx = canvas.getContext('2d');
      time += SPEED;
      var now = Date.now();
      var W = canvas.clientWidth, H = canvas.clientHeight;
      var GD = 80, rw = W*0.85, ro = (W-rw)/2;

      ctx.fillStyle = BG;
      ctx.fillRect(0, 0, W, H);
      ctx.strokeStyle = LC;
      ctx.lineWidth = 0.5;

      for (var i=0; i<GD; i++) {
        var lx = ro+(i/GD)*rw;
        ctx.beginPath();
        for (var j=0; j<=GD; j++) {
          var lp = (j/GD)*1.2-0.1, ly = lp*H;
          var ld = deform(lx, ly, time, lp, now);
          j===0 ? ctx.moveTo(lx+ld.ox, ly+ld.oy) : ctx.lineTo(lx+ld.ox, ly+ld.oy);
        }
        ctx.stroke();
      }

      for (var k=0; k<GD; k++) {
        var hp = (k/GD)*1.2-0.1, hy = hp*H;
        ctx.beginPath();
        for (var m=0; m<=GD; m++) {
          var hx = ro+(m/GD)*rw;
          var hd = deform(hx, hy, time, hp, now);
          m===0 ? ctx.moveTo(hx+hd.ox, hy+hd.oy) : ctx.lineTo(hx+hd.ox, hy+hd.oy);
        }
        ctx.stroke();
      }

      if (!REMOVE_WAVE) {
        for (var di=0; di<disturbances.length; di++) {
          var dist2 = disturbances[di];
          var age2 = now-dist2.t;
          if (age2 < 3000) {
            var prog = age2/3000;
            ctx.beginPath();
            ctx.strokeStyle = 'rgba(100,100,100,'+(1-prog)*0.2*dist2.i+')';
            ctx.lineWidth = 2;
            ctx.arc(dist2.x, dist2.y, prog*400, 0, Math.PI*2);
            ctx.stroke();
            ctx.strokeStyle = LC;
            ctx.lineWidth = 0.5;
          }
        }
      }

      requestAnimationFrame(draw);
    }

    resize();
    window.addEventListener('resize', resize);
    canvas.addEventListener('mousemove', function(e) {
      var rect = canvas.getBoundingClientRect();
      mouse.x = e.clientX-rect.left; mouse.y = e.clientY-rect.top;
    });
    canvas.addEventListener('mousedown', function(e) {
      mouse.down = true;
      var rect = canvas.getBoundingClientRect();
      disturbances.push({ x: e.clientX-rect.left, y: e.clientY-rect.top, t: Date.now(), i: 2 });
      var now = Date.now();
      disturbances = disturbances.filter(function(d){ return now-d.t < 3000; });
    });
    canvas.addEventListener('mouseup', function(){ mouse.down = false; });
    draw();
  </script>
</body>
</html>

More Reactive Backgrounds Animations