1<!DOCTYPE html>
2<html lang="en">
3<head>
4<meta charset="UTF-8" />
5<meta name="viewport" content="width=device-width,initial-scale=1" />
6<title>Blob Wave Dots Animation</title>
7<style>
8 :root{
9 --bg:#2f2f2f;
10 --mag:#f000ff;
11 --red:#ff120d;
12 }
13 html,body{
14 margin:0;
15 height:100%;
16 background:var(--bg);
17 overflow:hidden;
18 font-family:sans-serif;
19 }
20 canvas{
21 display:block;
22 width:100vw;
23 height:100vh;
24 }
25</style>
26</head>
27<body>
28<canvas id="c"></canvas>
29<script>
30const canvas = document.getElementById('c');
31const ctx = canvas.getContext('2d');
32
33function resize(){
34 const dpr = Math.min(devicePixelRatio || 1, 2);
35 canvas.width = innerWidth * dpr;
36 canvas.height = innerHeight * dpr;
37 canvas.style.width = innerWidth + 'px';
38 canvas.style.height = innerHeight + 'px';
39 ctx.setTransform(dpr,0,0,dpr,0,0);
40}
41addEventListener('resize', resize);
42resize();
43
44const TAU = Math.PI * 2;
45const lerp = (a,b,t)=>a+(b-a)*t;
46const clamp = (v,a,b)=>Math.max(a,Math.min(b,v));
47const smooth = t => t*t*(3-2*t);
48const easeInOut = t => 0.5 - 0.5*Math.cos(Math.PI*t);
49
50function gradColor(t){
51 t = clamp(t,0,1);
52 const c1 = [240,0,255];
53 const c2 = [255,18,13];
54 const r = Math.round(lerp(c1[0],c2[0],t));
55 const g = Math.round(lerp(c1[1],c2[1],t));
56 const b = Math.round(lerp(c1[2],c2[2],t));
57 return `rgb(${r},${g},${b})`;
58}
59
60function pulse(x, c, w){
61 const d = Math.abs(x-c);
62 if(d >= w) return 0;
63 return 0.5 + 0.5*Math.cos(Math.PI * d / w);
64}
65
66function drawCircle(x,y,r,color){
67 ctx.beginPath();
68 ctx.arc(x,y,r,0,TAU);
69 ctx.fillStyle = color;
70 ctx.fill();
71}
72
73function metaballChain(points, baseR, ampFn, colorFn){
74 const step = 4;
75 for(let x = points[0].x - baseR; x <= points[points.length-1].x + baseR; x += step){
76 let top = Infinity, bot = -Infinity, hit = false;
77 for(let i=0;i<points.length;i++){
78 const p = points[i];
79 const dx = x - p.x;
80 const r = baseR + ampFn(i,p);
81 if(Math.abs(dx) <= r){
82 const h = Math.sqrt(r*r - dx*dx);
83 top = Math.min(top, p.y - h);
84 bot = Math.max(bot, p.y + h);
85 hit = true;
86 }
87 }
88 if(hit){
89 ctx.strokeStyle = colorFn((x - points[0].x)/(points[points.length-1].x - points[0].x));
90 ctx.lineWidth = step + 1;
91 ctx.beginPath();
92 ctx.moveTo(x, top);
93 ctx.lineTo(x, bot);
94 ctx.stroke();
95 }
96 }
97}
98
99function draw(){
100 const w = innerWidth, h = innerHeight;
101 ctx.clearRect(0,0,w,h);
102
103 const left = w * 0.145;
104 const right = w * 0.812;
105 const topY = h * 0.245;
106 const botY = h * 0.415;
107
108 const n = 16;
109 const xs = [];
110 for(let i=0;i<n;i++) xs.push(lerp(left,right,i/(n-1)));
111
112 const t = (performance.now() * 0.001);
113 const cycle = 6.4;
114 const p = (t % cycle) / cycle; // 0..1
115
116 // traveling disturbance left -> right
117 const head = lerp(-0.06, 1.08, p);
118 const width = 0.18;
119
120 // top chain points
121 const topPts = xs.map((x,i)=>({x,y:topY}));
122 metaballChain(
123 topPts,
124 18,
125 (i,pnt)=>{
126 const u = i/(n-1);
127 const wave = 2.2*Math.sin(i*1.9 + t*10.5);
128 const bump = 7 * pulse(u, head, width);
129 return wave + bump;
130 },
131 u=>gradColor(u)
132 );
133
134 // lower dotted guide row
135 for(let i=0;i<n;i++){
136 const u = i/(n-1);
137 const x = xs[i];
138 const local = pulse(u, head, width*1.15);
139 const y = botY + 10*Math.sin((u*8.5 - p*7.5)*Math.PI) * local;
140 const r = lerp(2.6,4.2, local*0.9);
141 drawCircle(x,y,r,gradColor(u));
142 }
143
144 // bouncing detached blobs between rows
145 for(let i=0;i<n;i++){
146 const u = i/(n-1);
147 const influence = pulse(u, head, width);
148 if(influence < 0.02) continue;
149
150 const phase = clamp((head - u)/width * 0.5 + 0.5, 0, 1);
151 const arc = Math.sin(phase * Math.PI);
152 const x = xs[i];
153 const y = lerp(botY - 28, topY + 8, arc) - 18*influence*Math.sin((u*11 - p*9)*Math.PI);
154 const r = 7 + 12*influence*(0.45 + 0.55*arc);
155 drawCircle(x,y,r,gradColor(u));
156 }
157
158 // downward drips from top bar
159 for(let i=0;i<n;i++){
160 const u = i/(n-1);
161 const lagHead = head - 0.08;
162 const inf = pulse(u, lagHead, width*0.72);
163 if(inf < 0.12) continue;
164
165 const drop = 10 + 28*inf;
166 const x = xs[i];
167 const r = 8 + 6*inf;
168 ctx.strokeStyle = gradColor(u);
169 ctx.lineCap = 'round';
170 ctx.lineWidth = r*1.55;
171 ctx.beginPath();
172 ctx.moveTo(x, topY + 2);
173 ctx.lineTo(x, topY + drop);
174 ctx.stroke();
175 drawCircle(x, topY + drop + r*0.15, r, gradColor(u));
176 }
177
178 requestAnimationFrame(draw);
179}
180draw();
181</script>
182</body>
183</html>