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>