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>Floating Bubbles</title>
7<style>
8 :root{
9 --bg:#1f2329;
10 --bubble: rgba(110, 180, 190, 0.58);
11 }
12
13 html,body{
14 margin:0;
15 width:100%;
16 height:100%;
17 overflow:hidden;
18 background:var(--bg);
19 }
20
21 body{
22 position:relative;
23 font-family:sans-serif;
24 }
25
26 #scene{
27 position:fixed;
28 inset:0;
29 overflow:hidden;
30 background:var(--bg);
31 }
32
33 .bubble{
34 position:absolute;
35 border-radius:50%;
36 background:var(--bubble);
37 transform:translate3d(0,0,0);
38 will-change:transform, opacity;
39 pointer-events:none;
40 }
41</style>
42</head>
43<body>
44<div id="scene"></div>
45
46<script>
47(() => {
48 const scene = document.getElementById('scene');
49 const W = () => innerWidth;
50 const H = () => innerHeight;
51
52 const bubbles = [];
53 const TOTAL = 26;
54
55 // Slow start like the reference: mostly empty, then bubbles rise in.
56 function spawn(i){
57 const el = document.createElement('div');
58 el.className = 'bubble';
59
60 const sideBias = Math.random();
61 let x;
62 if (sideBias < 0.38) x = Math.random() * W() * 0.28; // left cluster
63 else if (sideBias > 0.62) x = W() * (0.72 + Math.random() * 0.28); // right cluster
64 else x = W() * (0.25 + Math.random() * 0.5); // occasional center
65
66 const r = 6 + Math.random() * 34;
67 const y = H() + r + Math.random() * H() * 0.55;
68
69 const speed = 12 + (42 - r) * 0.22 + Math.random() * 10; // px/sec, larger bubbles slightly slower
70 const drift = (Math.random() - 0.5) * 18;
71 const phase = Math.random() * Math.PI * 2;
72 const amp = 6 + Math.random() * 18;
73 const opacity = 0.42 + Math.random() * 0.22;
74
75 el.style.width = el.style.height = `${r * 2}px`;
76 el.style.opacity = opacity.toFixed(3);
77 scene.appendChild(el);
78
79 bubbles.push({
80 el, x, y, r, speed, drift, phase, amp,
81 born: performance.now() + i * 420 + (i < 2 ? 2600 : 0) // long empty intro, then gradual appearance
82 });
83 }
84
85 for(let i=0;i<TOTAL;i++) spawn(i);
86
87 function resetBubble(b, delay = 0){
88 const sideBias = Math.random();
89 if (sideBias < 0.42) b.x = Math.random() * W() * 0.3;
90 else if (sideBias > 0.58) b.x = W() * (0.7 + Math.random() * 0.3);
91 else b.x = W() * (0.2 + Math.random() * 0.6);
92
93 b.r = 6 + Math.random() * 34;
94 b.y = H() + b.r + Math.random() * 120;
95 b.speed = 12 + (42 - b.r) * 0.22 + Math.random() * 10;
96 b.drift = (Math.random() - 0.5) * 18;
97 b.phase = Math.random() * Math.PI * 2;
98 b.amp = 6 + Math.random() * 18;
99 b.born = performance.now() + delay;
100
101 b.el.style.width = b.el.style.height = `${b.r * 2}px`;
102 b.el.style.opacity = (0.42 + Math.random() * 0.22).toFixed(3);
103 }
104
105 let last = performance.now();
106
107 function tick(now){
108 const dt = Math.min(0.033, (now - last) / 1000);
109 last = now;
110
111 for(const b of bubbles){
112 if(now < b.born){
113 b.el.style.transform = `translate3d(-9999px,-9999px,0)`;
114 continue;
115 }
116
117 b.y -= b.speed * dt;
118
119 const t = now * 0.001;
120 const x = b.x + Math.sin(t * 0.8 + b.phase) * b.amp + b.drift * (t * 0.12);
121 const y = b.y;
122
123 b.el.style.transform = `translate3d(${x - b.r}px, ${y - b.r}px, 0)`;
124
125 if(y < -b.r * 2 - 20){
126 resetBubble(b, 300 + Math.random() * 1800);
127 }
128 }
129
130 requestAnimationFrame(tick);
131 }
132
133 addEventListener('resize', () => {
134 for(const b of bubbles){
135 b.x = Math.max(-40, Math.min(W() + 40, b.x));
136 b.y = Math.max(-100, Math.min(H() + 200, b.y));
137 }
138 });
139
140 requestAnimationFrame(tick);
141})();
142</script>
143</body>
144</html>