Aleksmorshen commited on
Commit
9cef53f
·
verified ·
1 Parent(s): 5119566

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +1227 -382
index.html CHANGED
@@ -1,401 +1,1246 @@
1
  <!DOCTYPE html>
2
  <html lang="ru">
3
  <head>
4
- <meta charset="UTF-8" />
5
- <title>Воксельная 4K-сцена</title>
6
- <style>
7
- html, body {
8
- margin: 0;
9
- padding: 0;
10
- overflow: hidden;
11
- background: #000;
12
- height: 100%;
13
- font-family: sans-serif;
14
- }
15
- #info {
16
- position: absolute;
17
- top: 10px; left: 10px;
18
- color: #fff;
19
- background: rgba(0,0,0,0.4);
20
- padding: 8px 12px;
21
- border-radius: 4px;
22
- font-size: 12px;
23
- z-index: 10;
24
- }
25
- #info b {
26
- color: #ffd66b;
27
- }
28
- </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  </head>
30
  <body>
31
- <div id="info">
32
- Воксельная 3D-сцена (приблизительный стиль Minecraft в 4K)<br>
33
- <b>Управление:</b> ЛКМ — вращение, ПКМ/колёсико — панорамирование, колесо — масштаб.<br>
34
- Разрешение можно сменить в коде: <b>targetWidth / targetHeight</b>.
35
- </div>
36
-
37
- <!-- Three.js с CDN -->
38
- <script src="https://cdn.jsdelivr.net/npm/three@0.161.0/build/three.min.js"></script>
39
- <script src="https://cdn.jsdelivr.net/npm/three@0.161.0/examples/js/controls/OrbitControls.js"></script>
40
-
41
- <script>
42
- /* === БАЗОВАЯ СЦЕНА === */
43
- const scene = new THREE.Scene();
44
- scene.background = new THREE.Color(0x87ceeb); // небо
45
-
46
- // Камера (ориентир под 4K, позже подгоним размер канваса)
47
- const targetWidth = 3840; // 4K ширина
48
- const targetHeight = 2160; // 4K высота
49
- const aspect = targetWidth / targetHeight;
50
-
51
- const camera = new THREE.PerspectiveCamera(60, aspect, 0.1, 2000);
52
- camera.position.set(60, 50, 80);
53
- camera.lookAt(0, 0, 0);
54
-
55
- // Рендерер
56
- const renderer = new THREE.WebGLRenderer({ antialias: true });
57
- renderer.setSize(targetWidth, targetHeight, false);
58
- renderer.setPixelRatio(window.devicePixelRatio > 2 ? 2 : window.devicePixelRatio);
59
- renderer.outputEncoding = THREE.sRGBEncoding;
60
- document.body.appendChild(renderer.domElement);
61
-
62
- // Подгонять канвас под окно, сохраняя соотношение
63
- function onWindowResize() {
64
- const w = window.innerWidth;
65
- const h = window.innerHeight;
66
- const windowAspect = w / h;
67
-
68
- if (windowAspect > aspect) {
69
- // окно шире вписываем по высоте
70
- const newWidth = h * aspect;
71
- renderer.setSize(newWidth, h, false);
72
- renderer.domElement.style.width = newWidth + "px";
73
- renderer.domElement.style.height = h + "px";
74
- } else {
75
- // окно уже — вписываем по ширине
76
- const newHeight = w / aspect;
77
- renderer.setSize(w, newHeight, false);
78
- renderer.domElement.style.width = w + "px";
79
- renderer.domElement.style.height = newHeight + "px";
80
- }
81
- camera.aspect = aspect;
82
- camera.updateProjectionMatrix();
83
- }
84
- window.addEventListener('resize', onWindowResize);
85
- onWindowResize();
86
-
87
- // Управление камерой
88
- const controls = new THREE.OrbitControls(camera, renderer.domElement);
89
- controls.enableDamping = true;
90
- controls.dampingFactor = 0.08;
91
- controls.target.set(32, 10, 32);
92
-
93
- /* === ОСВЕЩЕНИЕ === */
94
- const hemiLight = new THREE.HemisphereLight(0xffffff, 0x404040, 0.7);
95
- scene.add(hemiLight);
96
-
97
- const dirLight = new THREE.DirectionalLight(0xffffff, 1.0);
98
- dirLight.position.set(100, 120, 80);
99
- dirLight.castShadow = true;
100
- dirLight.shadow.mapSize.set(2048, 2048);
101
- dirLight.shadow.camera.near = 1;
102
- dirLight.shadow.camera.far = 400;
103
- dirLight.shadow.camera.left = -150;
104
- dirLight.shadow.camera.right = 150;
105
- dirLight.shadow.camera.top = 150;
106
- dirLight.shadow.camera.bottom = -150;
107
- scene.add(dirLight);
108
-
109
- /* === МАТЕРИАЛЫ ВОКСЕЛЕЙ === */
110
- const textureLoader = new THREE.TextureLoader();
111
- function makeRepeatingTexture(colorHex, repeat) {
112
- // генерация простой белой текстуры и окрашивание через color — дешевле
113
- const mat = new THREE.MeshStandardMaterial({
114
- color: colorHex,
115
- roughness: 1.0,
116
- metalness: 0.0
117
- });
118
- return mat;
119
- }
120
-
121
- // Примитивный "minecraft-подобный" набор
122
- const matGrassTop = new THREE.MeshStandardMaterial({ color: 0x76c445, roughness: 0.9 });
123
- const matDirt = new THREE.MeshStandardMaterial({ color: 0x8b5a2b, roughness: 1.0 });
124
- const matStone = new THREE.MeshStandardMaterial({ color: 0x888888, roughness: 1.0 });
125
- const matWater = new THREE.MeshStandardMaterial({ color: 0x2f66cc, roughness: 0.3, metalness: 0.1, transparent: true, opacity: 0.7 });
126
- const matWood = new THREE.MeshStandardMaterial({ color: 0x8b6b3f, roughness: 1.0 });
127
- const matLeaves = new THREE.MeshStandardMaterial({ color: 0x2f8a34, roughness: 0.9 });
128
- const matCloud = new THREE.MeshStandardMaterial({ color: 0xf9f9ff, roughness: 0.9, transparent: true, opacity: 0.9 });
129
- const matSmoke = new THREE.MeshStandardMaterial({ color: 0x555555, roughness: 0.9, transparent: true, opacity: 0.6 });
130
- const matSand = new THREE.MeshStandardMaterial({ color: 0xdacb8a, roughness: 0.9 });
131
-
132
- /* === ВОКСЕЛЬНЫЙ ГЕОМ === */
133
- const boxGeo = new THREE.BoxGeometry(1, 1, 1);
134
-
135
- // Функция создания блока
136
- function addBlock(x, y, z, material, parent) {
137
- const m = new THREE.Mesh(boxGeo, material);
138
- m.position.set(x + 0.5, y + 0.5, z + 0.5);
139
- m.castShadow = true;
140
- m.receiveShadow = true;
141
- parent.add(m);
142
- }
143
-
144
- /* === ГЕНЕРАЦИЯ ТЕРРАИНА === */
145
-
146
- // Простая "шумоподобная" функция
147
- function pseudoNoise(x, z) {
148
- // детерминированный псевдо-шум (не настоящий перлин, но сгодится)
149
- const s = Math.sin(x * 12.9898 + z * 78.233) * 43758.5453;
150
- return (s - Math.floor(s));
151
- }
152
-
153
- // Размер мира
154
- const worldSizeX = 64;
155
- const worldSizeZ = 64;
156
-
157
- const worldGroup = new THREE.Group();
158
- scene.add(worldGroup);
159
-
160
- // Поверхность: трава/земля, с "горами", камнем и небольшим озером
161
- for (let x = 0; x < worldSizeX; x++) {
162
- for (let z = 0; z < worldSizeZ; z++) {
163
- const n = pseudoNoise(x * 0.15, z * 0.15);
164
- let height = Math.floor(6 + n * 12); // от 6 до ~18
165
-
166
- // Лёгкое сглаживание по второму шуму
167
- const n2 = pseudoNoise(x * 0.05 + 50, z * 0.05 - 20);
168
- height += Math.floor(n2 * 4 - 2);
169
-
170
- if (height < 3) height = 3;
171
-
172
- // Вода - низкая точка
173
- const waterLevel = 7;
174
-
175
- for (let y = 0; y <= height; y++) {
176
- let mat;
177
- if (y === height) {
178
- // Верхний слой: трава/песок у воды/каменные вершины
179
- if (height > 15) {
180
- mat = matStone;
181
- } else if (y < waterLevel + 1 && Math.random() < 0.6) {
182
- mat = matSand;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
  } else {
184
- mat = matGrassTop;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
  } else {
187
- // Под землёй
188
- if (y < height - 3) {
189
- mat = matStone;
190
- } else {
191
- mat = matDirt;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
  }
193
  }
194
- addBlock(x, y, z, mat, worldGroup);
195
  }
196
 
197
- // Вода заполняет низины
198
- if (height < waterLevel) {
199
- for (let y = height + 1; y <= waterLevel; y++) {
200
- addBlock(x, y, z, matWater, worldGroup);
 
 
 
 
 
 
201
  }
 
202
  }
203
- }
204
- }
205
-
206
- /* === ДЕРЕВЬЯ === */
207
- function addTree(baseX, baseZ, baseY) {
208
- const trunkHeight = 4 + Math.floor(Math.random() * 2);
209
- // ствол
210
- for (let y = 0; y < trunkHeight; y++) {
211
- addBlock(baseX, baseY + y, baseZ, matWood, worldGroup);
212
- }
213
- const topY = baseY + trunkHeight;
214
-
215
- // крона (простая сфера из кубиков)
216
- const radius = 2 + Math.floor(Math.random() * 1.5);
217
- for (let x = -radius; x <= radius; x++) {
218
- for (let y = -radius; y <= radius; y++) {
219
- for (let z = -radius; z <= radius; z++) {
220
- if (x*x + y*y + z*z <= radius * radius + 1) {
221
- if (!(x === 0 && y <= 0 && z === 0)) {
222
- addBlock(baseX + x, topY + y, baseZ + z, matLeaves, worldGroup);
223
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
224
  }
225
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
226
  }
227
- }
228
- }
229
-
230
- // Расставим деревья по миру (на траве и не в воде)
231
- for (let i = 0; i < 45; i++) {
232
- const x = Math.floor(Math.random() * worldSizeX);
233
- const z = Math.floor(Math.random() * worldSizeZ);
234
-
235
- // Найдём наибольшую высоту в этой колонке
236
- let topY = -1;
237
- worldGroup.children.forEach(c => {
238
- const px = Math.floor(c.position.x);
239
- const pz = Math.floor(c.position.z);
240
- if (px === x && pz === z) {
241
- const py = Math.floor(c.position.y);
242
- if (py > topY) topY = py;
243
- }
244
- });
245
-
246
- if (topY < 2) continue;
247
-
248
- // Проверка: верхний блок — трава
249
- let topMat = null;
250
- worldGroup.children.forEach(c => {
251
- const px = Math.floor(c.position.x);
252
- const py = Math.floor(c.position.y);
253
- const pz = Math.floor(c.position.z);
254
- if (px === x && pz === z && py === topY) topMat = c.material;
255
- });
256
-
257
- if (topMat === matGrassTop && Math.random() < 0.5) {
258
- addTree(x, z, topY + 1);
259
- }
260
- }
261
-
262
- /* === ОБЛАКА === */
263
- const cloudsGroup = new THREE.Group();
264
- scene.add(cloudsGroup);
265
-
266
- function makeCloud(cx, cy, cz, size = 6) {
267
- const cloudCluster = new THREE.Group();
268
- const blocks = 12 + Math.floor(Math.random() * 10);
269
-
270
- for (let i = 0; i < blocks; i++) {
271
- const ox = (Math.random() - 0.5) * size;
272
- const oy = (Math.random() - 0.5) * (size * 0.4);
273
- const oz = (Math.random() - 0.5) * size;
274
- const cube = new THREE.Mesh(boxGeo, matCloud.clone());
275
- cube.scale.setScalar(1.6 + Math.random() * 0.7);
276
- cube.position.set(cx + ox, cy + oy, cz + oz);
277
- cube.castShadow = false;
278
- cube.receiveShadow = false;
279
- cloudCluster.add(cube);
280
- }
281
- cloudsGroup.add(cloudCluster);
282
- }
283
-
284
- // Несколько кучек облаков
285
- for (let i = 0; i < 12; i++) {
286
- const cx = (Math.random() - 0.5) * 200;
287
- const cz = (Math.random() - 0.5) * 200;
288
- const cy = 60 + Math.random() * 20;
289
- makeCloud(cx, cy, cz, 12 + Math.random() * 6);
290
- }
291
-
292
- /* === ДЫМ ИЗ ТРУБЫ / КОСТРА === */
293
- const smokeGroup = new THREE.Group();
294
- scene.add(smokeGroup);
295
-
296
- // "Костёр" в центре
297
- const fireX = worldSizeX / 2;
298
- const fireZ = worldSizeZ / 2;
299
-
300
- // найдём высоту в центре
301
- let centerHeight = 0;
302
- worldGroup.children.forEach(c => {
303
- const px = Math.floor(c.position.x);
304
- const pz = Math.floor(c.position.z);
305
- const py = Math.floor(c.position.y);
306
- if (px === fireX && pz === fireZ && py > centerHeight) {
307
- centerHeight = py;
308
- }
309
- });
310
-
311
- // подложим "брёвна"
312
- for (let ix = -1; ix <= 1; ix++) {
313
- for (let iz = -1; iz <= 1; iz++) {
314
- if (Math.abs(ix) !== Math.abs(iz)) {
315
- addBlock(fireX + ix, centerHeight + 1, fireZ + iz, matWood, worldGroup);
316
- }
317
- }
318
- }
319
-
320
- // параметры дыма
321
- const smokeParticles = [];
322
- const maxSmoke = 120;
323
-
324
- function spawnSmokeParticle() {
325
- if (smokeParticles.length >= maxSmoke) return;
326
- const s = new THREE.Mesh(boxGeo, matSmoke.clone());
327
- s.scale.set(0.8, 0.8, 0.8);
328
- s.position.set(
329
- fireX + 0.5 + (Math.random() - 0.5) * 0.4,
330
- centerHeight + 2 + Math.random() * 0.4,
331
- fireZ + 0.5 + (Math.random() - 0.5) * 0.4
332
- );
333
- s.material.opacity = 0.0;
334
- s.userData = {
335
- life: 0,
336
- maxLife: 5 + Math.random() * 4,
337
- riseSpeed: 1.2 + Math.random() * 0.8,
338
- driftX: (Math.random() - 0.5) * 0.25,
339
- driftZ: (Math.random() - 0.5) * 0.25
340
- };
341
- smokeGroup.add(s);
342
- smokeParticles.push(s);
343
- }
344
-
345
- /* === АНИМАЦИЯ === */
346
- let lastTime = performance.now();
347
-
348
- function animate() {
349
- requestAnimationFrame(animate);
350
-
351
- const now = performance.now();
352
- const dt = (now - lastTime) / 1000;
353
- lastTime = now;
354
-
355
- controls.update();
356
-
357
- // Движение облаков
358
- cloudsGroup.children.forEach(cluster => {
359
- cluster.position.x += 0.2 * dt * 20; // плавный сдвиг
360
- if (cluster.position.x > 150) cluster.position.x = -150;
361
- });
362
-
363
- // Периодический "выброс" дыма
364
- if (Math.random() < 0.2) {
365
- spawnSmokeParticle();
366
- }
367
-
368
- // Обновление частиц дыма
369
- for (let i = smokeParticles.length - 1; i >= 0; i--) {
370
- const p = smokeParticles[i];
371
- const data = p.userData;
372
- data.life += dt;
373
-
374
- const t = data.life / data.maxLife;
375
- p.position.y += data.riseSpeed * dt;
376
- p.position.x += data.driftX * dt;
377
- p.position.z += data.driftZ * dt;
378
-
379
- // рост и рассеивание
380
- const scale = 0.8 + t * 3;
381
- p.scale.set(scale, scale, scale);
382
-
383
- // появление и исчезновение
384
- if (t < 0.2) {
385
- p.material.opacity = t * 3;
386
- } else if (t > 0.7) {
387
- p.material.opacity = Math.max(0, 1 - (t - 0.7) / 0.3);
388
- }
389
-
390
- if (data.life >= data.maxLife || p.material.opacity <= 0) {
391
- smokeGroup.remove(p);
392
- smokeParticles.splice(i, 1);
393
- }
394
- }
395
-
396
- renderer.render(scene, camera);
397
- }
398
- animate();
399
- </script>
400
  </body>
401
  </html>
 
1
  <!DOCTYPE html>
2
  <html lang="ru">
3
  <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
6
+ <title>Симуляция воды</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ overflow: hidden;
16
+ background: #000;
17
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
18
+ touch-action: none;
19
+ }
20
+
21
+ #canvas {
22
+ display: block;
23
+ width: 100vw;
24
+ height: 100vh;
25
+ }
26
+
27
+ #controls {
28
+ position: fixed;
29
+ top: 10px;
30
+ left: 10px;
31
+ background: rgba(0, 0, 0, 0.85);
32
+ padding: 15px;
33
+ border-radius: 12px;
34
+ color: #fff;
35
+ max-width: 320px;
36
+ max-height: 90vh;
37
+ overflow-y: auto;
38
+ z-index: 100;
39
+ backdrop-filter: blur(10px);
40
+ border: 1px solid rgba(255, 255, 255, 0.1);
41
+ transition: transform 0.3s ease;
42
+ }
43
+
44
+ #controls.hidden {
45
+ transform: translateX(-110%);
46
+ }
47
+
48
+ #toggleBtn {
49
+ position: fixed;
50
+ top: 10px;
51
+ left: 10px;
52
+ width: 44px;
53
+ height: 44px;
54
+ background: rgba(0, 0, 0, 0.85);
55
+ border: 1px solid rgba(255, 255, 255, 0.2);
56
+ border-radius: 10px;
57
+ color: #fff;
58
+ font-size: 24px;
59
+ cursor: pointer;
60
+ z-index: 99;
61
+ display: none;
62
+ backdrop-filter: blur(10px);
63
+ }
64
+
65
+ #controls.hidden + #toggleBtn {
66
+ display: flex;
67
+ align-items: center;
68
+ justify-content: center;
69
+ }
70
+
71
+ .section {
72
+ margin-bottom: 15px;
73
+ padding-bottom: 15px;
74
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
75
+ }
76
+
77
+ .section:last-child {
78
+ border-bottom: none;
79
+ margin-bottom: 0;
80
+ padding-bottom: 0;
81
+ }
82
+
83
+ h3 {
84
+ font-size: 14px;
85
+ margin-bottom: 10px;
86
+ color: #4fc3f7;
87
+ display: flex;
88
+ align-items: center;
89
+ gap: 8px;
90
+ }
91
+
92
+ .control-row {
93
+ display: flex;
94
+ align-items: center;
95
+ margin-bottom: 8px;
96
+ gap: 10px;
97
+ }
98
+
99
+ label {
100
+ flex: 1;
101
+ font-size: 12px;
102
+ color: #aaa;
103
+ }
104
+
105
+ input[type="range"] {
106
+ flex: 1.5;
107
+ height: 6px;
108
+ border-radius: 3px;
109
+ background: #333;
110
+ outline: none;
111
+ -webkit-appearance: none;
112
+ }
113
+
114
+ input[type="range"]::-webkit-slider-thumb {
115
+ -webkit-appearance: none;
116
+ width: 18px;
117
+ height: 18px;
118
+ background: #4fc3f7;
119
+ border-radius: 50%;
120
+ cursor: pointer;
121
+ }
122
+
123
+ input[type="color"] {
124
+ width: 40px;
125
+ height: 30px;
126
+ border: none;
127
+ border-radius: 5px;
128
+ cursor: pointer;
129
+ }
130
+
131
+ .value {
132
+ min-width: 45px;
133
+ text-align: right;
134
+ font-size: 11px;
135
+ color: #4fc3f7;
136
+ font-family: monospace;
137
+ }
138
+
139
+ button {
140
+ width: 100%;
141
+ padding: 10px;
142
+ margin-top: 5px;
143
+ background: linear-gradient(135deg, #4fc3f7, #0288d1);
144
+ border: none;
145
+ border-radius: 8px;
146
+ color: #fff;
147
+ font-size: 13px;
148
+ cursor: pointer;
149
+ transition: all 0.2s;
150
+ }
151
+
152
+ button:hover {
153
+ transform: scale(1.02);
154
+ box-shadow: 0 4px 15px rgba(79, 195, 247, 0.4);
155
+ }
156
+
157
+ button:active {
158
+ transform: scale(0.98);
159
+ }
160
+
161
+ .close-btn {
162
+ position: absolute;
163
+ top: 10px;
164
+ right: 10px;
165
+ background: none;
166
+ border: none;
167
+ color: #888;
168
+ font-size: 20px;
169
+ cursor: pointer;
170
+ width: auto;
171
+ padding: 5px;
172
+ margin: 0;
173
+ }
174
+
175
+ .preset-grid {
176
+ display: grid;
177
+ grid-template-columns: 1fr 1fr;
178
+ gap: 8px;
179
+ margin-top: 10px;
180
+ }
181
+
182
+ .preset-grid button {
183
+ padding: 8px;
184
+ font-size: 11px;
185
+ }
186
+
187
+ #fps {
188
+ position: fixed;
189
+ top: 10px;
190
+ right: 10px;
191
+ background: rgba(0, 0, 0, 0.7);
192
+ padding: 8px 12px;
193
+ border-radius: 8px;
194
+ color: #4fc3f7;
195
+ font-size: 12px;
196
+ font-family: monospace;
197
+ z-index: 100;
198
+ }
199
+
200
+ @media (max-width: 768px) {
201
+ #controls {
202
+ max-width: 280px;
203
+ padding: 12px;
204
+ font-size: 11px;
205
+ }
206
+
207
+ h3 {
208
+ font-size: 13px;
209
+ }
210
+
211
+ input[type="range"] {
212
+ flex: 1;
213
+ }
214
+
215
+ .value {
216
+ min-width: 35px;
217
+ font-size: 10px;
218
+ }
219
+ }
220
+
221
+ @media (max-width: 400px) {
222
+ #controls {
223
+ max-width: calc(100vw - 20px);
224
+ left: 5px;
225
+ right: 5px;
226
+ }
227
+ }
228
+
229
+ .checkbox-row {
230
+ display: flex;
231
+ align-items: center;
232
+ gap: 10px;
233
+ margin-bottom: 8px;
234
+ }
235
+
236
+ .checkbox-row input[type="checkbox"] {
237
+ width: 18px;
238
+ height: 18px;
239
+ accent-color: #4fc3f7;
240
+ }
241
+
242
+ .checkbox-row label {
243
+ flex: none;
244
+ }
245
+ </style>
246
  </head>
247
  <body>
248
+ <canvas id="canvas"></canvas>
249
+
250
+ <div id="controls">
251
+ <button class="close-btn" onclick="toggleControls()">×</button>
252
+
253
+ <div class="section">
254
+ <h3>🌊 Физика волн</h3>
255
+ <div class="control-row">
256
+ <label>Скорость волн</label>
257
+ <input type="range" id="waveSpeed" min="0.1" max="3" step="0.1" value="1">
258
+ <span class="value" id="waveSpeedVal">1.0</span>
259
+ </div>
260
+ <div class="control-row">
261
+ <label>Затухание</label>
262
+ <input type="range" id="damping" min="0.9" max="0.999" step="0.001" value="0.995">
263
+ <span class="value" id="dampingVal">0.995</span>
264
+ </div>
265
+ <div class="control-row">
266
+ <label>Размер капли</label>
267
+ <input type="range" id="dropRadius" min="1" max="50" step="1" value="15">
268
+ <span class="value" id="dropRadiusVal">15</span>
269
+ </div>
270
+ <div class="control-row">
271
+ <label>Сила капли</label>
272
+ <input type="range" id="dropStrength" min="0.1" max="2" step="0.1" value="0.5">
273
+ <span class="value" id="dropStrengthVal">0.5</span>
274
+ </div>
275
+ <div class="control-row">
276
+ <label>Поверхн. натяж.</label>
277
+ <input type="range" id="tension" min="0" max="0.5" step="0.01" value="0.1">
278
+ <span class="value" id="tensionVal">0.10</span>
279
+ </div>
280
+ </div>
281
+
282
+ <div class="section">
283
+ <h3>🎨 Визуализация</h3>
284
+ <div class="control-row">
285
+ <label>Глубина воды</label>
286
+ <input type="range" id="waterDepth" min="0.1" max="2" step="0.1" value="0.8">
287
+ <span class="value" id="waterDepthVal">0.8</span>
288
+ </div>
289
+ <div class="control-row">
290
+ <label>Преломление</label>
291
+ <input type="range" id="refraction" min="0" max="1" step="0.05" value="0.3">
292
+ <span class="value" id="refractionVal">0.30</span>
293
+ </div>
294
+ <div class="control-row">
295
+ <label>Отражение</label>
296
+ <input type="range" id="reflection" min="0" max="1" step="0.05" value="0.4">
297
+ <span class="value" id="reflectionVal">0.40</span>
298
+ </div>
299
+ <div class="control-row">
300
+ <label>Каустика</label>
301
+ <input type="range" id="caustics" min="0" max="2" step="0.1" value="1">
302
+ <span class="value" id="causticsVal">1.0</span>
303
+ </div>
304
+ <div class="control-row">
305
+ <label>Яркость</label>
306
+ <input type="range" id="brightness" min="0.5" max="2" step="0.1" value="1">
307
+ <span class="value" id="brightnessVal">1.0</span>
308
+ </div>
309
+ <div class="control-row">
310
+ <label>Спекуляр</label>
311
+ <input type="range" id="specular" min="0" max="3" step="0.1" value="1.5">
312
+ <span class="value" id="specularVal">1.5</span>
313
+ </div>
314
+ </div>
315
+
316
+ <div class="section">
317
+ <h3>🎭 Цвета</h3>
318
+ <div class="control-row">
319
+ <label>Цвет воды</label>
320
+ <input type="color" id="waterColor" value="#0066aa">
321
+ </div>
322
+ <div class="control-row">
323
+ <label>Цвет глубины</label>
324
+ <input type="color" id="deepColor" value="#001133">
325
+ </div>
326
+ <div class="control-row">
327
+ <label>Цвет неба</label>
328
+ <input type="color" id="skyColor" value="#87ceeb">
329
+ </div>
330
+ <div class="control-row">
331
+ <label>Цвет пены</label>
332
+ <input type="color" id="foamColor" value="#ffffff">
333
+ </div>
334
+ </div>
335
+
336
+ <div class="section">
337
+ <h3>🌤️ Освещение</h3>
338
+ <div class="control-row">
339
+ <label>Направление X</label>
340
+ <input type="range" id="lightX" min="-1" max="1" step="0.1" value="0.5">
341
+ <span class="value" id="lightXVal">0.5</span>
342
+ </div>
343
+ <div class="control-row">
344
+ <label>Направление Y</label>
345
+ <input type="range" id="lightY" min="0.1" max="1" step="0.1" value="0.8">
346
+ <span class="value" id="lightYVal">0.8</span>
347
+ </div>
348
+ <div class="control-row">
349
+ <label>Направление Z</label>
350
+ <input type="range" id="lightZ" min="-1" max="1" step="0.1" value="0.3">
351
+ <span class="value" id="lightZVal">0.3</span>
352
+ </div>
353
+ </div>
354
+
355
+ <div class="section">
356
+ <h3>🌀 Автоволны</h3>
357
+ <div class="checkbox-row">
358
+ <input type="checkbox" id="autoWaves" checked>
359
+ <label>Включить дождь</label>
360
+ </div>
361
+ <div class="control-row">
362
+ <label>Частота</label>
363
+ <input type="range" id="rainFreq" min="0.01" max="0.3" step="0.01" value="0.05">
364
+ <span class="value" id="rainFreqVal">0.05</span>
365
+ </div>
366
+ <div class="checkbox-row">
367
+ <input type="checkbox" id="windWaves">
368
+ <label>Ветровые волны</label>
369
+ </div>
370
+ <div class="control-row">
371
+ <label>Сила ветра</label>
372
+ <input type="range" id="windStrength" min="0" max="0.01" step="0.001" value="0.003">
373
+ <span class="value" id="windStrengthVal">0.003</span>
374
+ </div>
375
+ </div>
376
+
377
+ <div class="section">
378
+ <h3>⚙️ Качество</h3>
379
+ <div class="control-row">
380
+ <label>Разрешение сим.</label>
381
+ <input type="range" id="simResolution" min="128" max="1024" step="64" value="512">
382
+ <span class="value" id="simResolutionVal">512</span>
383
+ </div>
384
+ <div class="checkbox-row">
385
+ <input type="checkbox" id="highQuality" checked>
386
+ <label>Высокое качество</label>
387
+ </div>
388
+ </div>
389
+
390
+ <div class="section">
391
+ <h3>🎮 Пресеты</h3>
392
+ <div class="preset-grid">
393
+ <button onclick="applyPreset('calm')">🌅 Спокойно</button>
394
+ <button onclick="applyPreset('storm')">⛈️ Шторм</button>
395
+ <button onclick="applyPreset('pool')">🏊 Бассейн</button>
396
+ <button onclick="applyPreset('ocean')">🌊 Океан</button>
397
+ <button onclick="applyPreset('toxic')">☢️ Токсично</button>
398
+ <button onclick="applyPreset('lava')">🌋 Лава</button>
399
+ </div>
400
+ <button onclick="resetWater()" style="margin-top:10px">🔄 Сбросить воду</button>
401
+ </div>
402
+ </div>
403
+
404
+ <button id="toggleBtn" onclick="toggleControls()">☰</button>
405
+ <div id="fps">FPS: 60</div>
406
+
407
+ <script>
408
+ // WebGL контекст и переменные
409
+ const canvas = document.getElementById('canvas');
410
+ let gl = canvas.getContext('webgl2') || canvas.getContext('webgl');
411
+ if (!gl) {
412
+ alert('WebGL не поддерживается');
413
+ throw new Error('WebGL not supported');
414
+ }
415
+
416
+ const isWebGL2 = gl instanceof WebGL2RenderingContext;
417
+
418
+ // Параметры симуляции
419
+ let params = {
420
+ waveSpeed: 1.0,
421
+ damping: 0.995,
422
+ dropRadius: 15,
423
+ dropStrength: 0.5,
424
+ tension: 0.1,
425
+ waterDepth: 0.8,
426
+ refraction: 0.3,
427
+ reflection: 0.4,
428
+ caustics: 1.0,
429
+ brightness: 1.0,
430
+ specular: 1.5,
431
+ waterColor: [0, 0.4, 0.67],
432
+ deepColor: [0, 0.07, 0.2],
433
+ skyColor: [0.53, 0.81, 0.92],
434
+ foamColor: [1, 1, 1],
435
+ lightX: 0.5,
436
+ lightY: 0.8,
437
+ lightZ: 0.3,
438
+ autoWaves: true,
439
+ rainFreq: 0.05,
440
+ windWaves: false,
441
+ windStrength: 0.003,
442
+ simResolution: 512,
443
+ highQuality: true
444
+ };
445
+
446
+ let simWidth = params.simResolution;
447
+ let simHeight = params.simResolution;
448
+
449
+ // Framebuffers для ping-pong симуляции
450
+ let framebuffers = [];
451
+ let textures = [];
452
+ let currentFB = 0;
453
+
454
+ // Шейдеры
455
+ const quadVS = `
456
+ attribute vec2 position;
457
+ varying vec2 vUv;
458
+ void main() {
459
+ vUv = position * 0.5 + 0.5;
460
+ gl_Position = vec4(position, 0.0, 1.0);
461
+ }
462
+ `;
463
+
464
+ // Шейдер симуляции волн (высокоточная модель)
465
+ const waveSimFS = `
466
+ precision highp float;
467
+ varying vec2 vUv;
468
+ uniform sampler2D uPrevState;
469
+ uniform sampler2D uCurrState;
470
+ uniform vec2 uResolution;
471
+ uniform float uDamping;
472
+ uniform float uSpeed;
473
+ uniform float uTension;
474
+ uniform float uDt;
475
+
476
+ void main() {
477
+ vec2 texel = 1.0 / uResolution;
478
+
479
+ // Текущие значения
480
+ vec4 curr = texture2D(uCurrState, vUv);
481
+ vec4 prev = texture2D(uPrevState, vUv);
482
+
483
+ // Соседние ячейки (9-точечный stencil для высокой точности)
484
+ float h = curr.r;
485
+ float hL = texture2D(uCurrState, vUv + vec2(-texel.x, 0.0)).r;
486
+ float hR = texture2D(uCurrState, vUv + vec2(texel.x, 0.0)).r;
487
+ float hT = texture2D(uCurrState, vUv + vec2(0.0, texel.y)).r;
488
+ float hB = texture2D(uCurrState, vUv + vec2(0.0, -texel.y)).r;
489
+
490
+ // Диагональные соседи для улучшенной точности
491
+ float hTL = texture2D(uCurrState, vUv + vec2(-texel.x, texel.y)).r;
492
+ float hTR = texture2D(uCurrState, vUv + vec2(texel.x, texel.y)).r;
493
+ float hBL = texture2D(uCurrState, vUv + vec2(-texel.x, -texel.y)).r;
494
+ float hBR = texture2D(uCurrState, vUv + vec2(texel.x, -texel.y)).r;
495
+
496
+ // 9-точечный Лапласиан для большей точности
497
+ float laplacian = (hL + hR + hT + hB) * 0.2 +
498
+ (hTL + hTR + hBL + hBR) * 0.05 - h;
499
+
500
+ // Поверхностное натяжение (билапласиан)
501
+ float tension = 0.0;
502
+ if (uTension > 0.0) {
503
+ float laplacian2 = (
504
+ texture2D(uCurrState, vUv + vec2(-2.0*texel.x, 0.0)).r +
505
+ texture2D(uCurrState, vUv + vec2(2.0*texel.x, 0.0)).r +
506
+ texture2D(uCurrState, vUv + vec2(0.0, 2.0*texel.y)).r +
507
+ texture2D(uCurrState, vUv + vec2(0.0, -2.0*texel.y)).r -
508
+ 4.0 * h
509
+ ) * 0.25 - laplacian;
510
+ tension = -uTension * laplacian2;
511
+ }
512
+
513
+ // Волновое уравнение с затуханием
514
+ float c2 = uSpeed * uSpeed;
515
+ float newH = 2.0 * h - prev.r + c2 * (laplacian + tension);
516
+ newH *= uDamping;
517
+
518
+ // Вычисление скорости для рендеринга
519
+ float velocity = (newH - prev.r) * 0.5;
520
+
521
+ // Вычисление нормали
522
+ float dx = (hR - hL) * 0.5;
523
+ float dy = (hT - hB) * 0.5;
524
+
525
+ gl_FragColor = vec4(newH, velocity, dx, dy);
526
+ }
527
+ `;
528
+
529
+ // Шейдер добавления капли
530
+ const dropFS = `
531
+ precision highp float;
532
+ varying vec2 vUv;
533
+ uniform sampler2D uState;
534
+ uniform vec2 uDropPos;
535
+ uniform float uDropRadius;
536
+ uniform float uDropStrength;
537
+ uniform vec2 uResolution;
538
+
539
+ void main() {
540
+ vec4 state = texture2D(uState, vUv);
541
+
542
+ vec2 pos = vUv * uResolution;
543
+ float dist = length(pos - uDropPos);
544
+
545
+ if (dist < uDropRadius) {
546
+ float factor = 1.0 - dist / uDropRadius;
547
+ factor = factor * factor * (3.0 - 2.0 * factor); // Smoothstep
548
+ state.r -= uDropStrength * factor;
549
+ }
550
+
551
+ gl_FragColor = state;
552
+ }
553
+ `;
554
+
555
+ // Шейдер рендеринга воды (высококачественный)
556
+ const renderFS = `
557
+ precision highp float;
558
+ varying vec2 vUv;
559
+ uniform sampler2D uState;
560
+ uniform sampler2D uBackground;
561
+ uniform vec2 uResolution;
562
+ uniform float uTime;
563
+ uniform vec3 uWaterColor;
564
+ uniform vec3 uDeepColor;
565
+ uniform vec3 uSkyColor;
566
+ uniform vec3 uFoamColor;
567
+ uniform vec3 uLightDir;
568
+ uniform float uRefraction;
569
+ uniform float uReflection;
570
+ uniform float uCaustics;
571
+ uniform float uBrightness;
572
+ uniform float uSpecular;
573
+ uniform float uWaterDepth;
574
+ uniform bool uHighQuality;
575
+
576
+ // Функция шума для деталей
577
+ float hash(vec2 p) {
578
+ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
579
+ }
580
+
581
+ float noise(vec2 p) {
582
+ vec2 i = floor(p);
583
+ vec2 f = fract(p);
584
+ f = f * f * (3.0 - 2.0 * f);
585
+
586
+ float a = hash(i);
587
+ float b = hash(i + vec2(1.0, 0.0));
588
+ float c = hash(i + vec2(0.0, 1.0));
589
+ float d = hash(i + vec2(1.0, 1.0));
590
+
591
+ return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
592
+ }
593
+
594
+ float fbm(vec2 p) {
595
+ float value = 0.0;
596
+ float amplitude = 0.5;
597
+ for (int i = 0; i < 4; i++) {
598
+ value += amplitude * noise(p);
599
+ p *= 2.0;
600
+ amplitude *= 0.5;
601
+ }
602
+ return value;
603
+ }
604
+
605
+ vec3 getCaustics(vec2 uv, float time) {
606
+ vec2 p = uv * 8.0;
607
+ float c = 0.0;
608
+ for (int i = 0; i < 3; i++) {
609
+ float t = time * (0.5 + float(i) * 0.2);
610
+ c += abs(sin(p.x * 3.0 + t) * cos(p.y * 2.5 - t * 0.7));
611
+ p = p * 1.5 + vec2(0.1, 0.2);
612
+ }
613
+ return vec3(c * 0.15);
614
+ }
615
+
616
+ void main() {
617
+ vec4 state = texture2D(uState, vUv);
618
+ float height = state.r;
619
+ float velocity = state.g;
620
+ vec2 gradient = state.ba;
621
+
622
+ // Высококачественная нормаль
623
+ vec2 texel = 1.0 / uResolution;
624
+ vec3 normal;
625
+
626
+ if (uHighQuality) {
627
+ // Собель-оператор для нормалей
628
+ float hL = texture2D(uState, vUv + vec2(-texel.x, 0.0)).r;
629
+ float hR = texture2D(uState, vUv + vec2(texel.x, 0.0)).r;
630
+ float hT = texture2D(uState, vUv + vec2(0.0, texel.y)).r;
631
+ float hB = texture2D(uState, vUv + vec2(0.0, -texel.y)).r;
632
+ float hTL = texture2D(uState, vUv + vec2(-texel.x, texel.y)).r;
633
+ float hTR = texture2D(uState, vUv + vec2(texel.x, texel.y)).r;
634
+ float hBL = texture2D(uState, vUv + vec2(-texel.x, -texel.y)).r;
635
+ float hBR = texture2D(uState, vUv + vec2(texel.x, -texel.y)).r;
636
+
637
+ float dx = (hTR + 2.0*hR + hBR - hTL - 2.0*hL - hBL) / 8.0;
638
+ float dy = (hTL + 2.0*hT + hTR - hBL - 2.0*hB - hBR) / 8.0;
639
+ normal = normalize(vec3(-dx * 30.0, -dy * 30.0, 1.0));
640
  } else {
641
+ normal = normalize(vec3(-gradient.x * 20.0, -gradient.y * 20.0, 1.0));
642
+ }
643
+
644
+ // Направление света
645
+ vec3 lightDir = normalize(uLightDir);
646
+ vec3 viewDir = vec3(0.0, 0.0, 1.0);
647
+ vec3 halfVec = normalize(lightDir + viewDir);
648
+
649
+ // Освещение
650
+ float diffuse = max(dot(normal, lightDir), 0.0);
651
+ float specularLight = pow(max(dot(normal, halfVec), 0.0), 64.0) * uSpecular;
652
+
653
+ // Френель
654
+ float fresnel = pow(1.0 - max(dot(normal, viewDir), 0.0), 3.0);
655
+ fresnel = mix(0.02, 1.0, fresnel);
656
+
657
+ // Преломление
658
+ vec2 refractedUv = vUv + normal.xy * uRefraction * 0.1;
659
+ refractedUv = clamp(refractedUv, 0.0, 1.0);
660
+
661
+ // Глубина и затухание
662
+ float depth = uWaterDepth + height * 0.3;
663
+ float absorption = exp(-depth * 2.0);
664
+
665
+ // Базовый цвет воды с градиентом глубины
666
+ vec3 waterCol = mix(uDeepColor, uWaterColor, absorption);
667
+
668
+ // Каустика
669
+ vec3 caustics = getCaustics(refractedUv, uTime) * uCaustics * absorption;
670
+
671
+ // Отражение неба
672
+ vec3 reflectedDir = reflect(-viewDir, normal);
673
+ float skyFactor = max(reflectedDir.z, 0.0);
674
+ vec3 skyReflection = mix(uSkyColor * 0.7, uSkyColor, skyFactor);
675
+
676
+ // Пена на гребнях волн
677
+ float foam = smoothstep(0.02, 0.08, height) * smoothstep(0.15, 0.08, height);
678
+ foam += smoothstep(0.0, 0.02, abs(velocity)) * 0.3;
679
+ foam = clamp(foam, 0.0, 1.0);
680
+
681
+ // Мелкие детали поверхности
682
+ if (uHighQuality) {
683
+ float detail = fbm(vUv * 50.0 + uTime * 0.5) * 0.02;
684
+ waterCol += vec3(detail);
685
  }
686
+
687
+ // Финальное смешивание
688
+ vec3 color = waterCol;
689
+ color += caustics;
690
+ color = mix(color, skyReflection, fresnel * uReflection);
691
+ color += vec3(specularLight);
692
+ color = mix(color, uFoamColor, foam * 0.5);
693
+ color *= uBrightness;
694
+ color *= 0.8 + diffuse * 0.4;
695
+
696
+ // Тонмэппинг
697
+ color = color / (color + vec3(1.0));
698
+ color = pow(color, vec3(1.0/2.2));
699
+
700
+ gl_FragColor = vec4(color, 1.0);
701
+ }
702
+ `;
703
+
704
+ // Шейдерные программы
705
+ let waveSimProgram, dropProgram, renderProgram;
706
+ let quadBuffer;
707
+
708
+ // Компиляция шейдера
709
+ function compileShader(source, type) {
710
+ const shader = gl.createShader(type);
711
+ gl.shaderSource(shader, source);
712
+ gl.compileShader(shader);
713
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
714
+ console.error('Shader error:', gl.getShaderInfoLog(shader));
715
+ return null;
716
+ }
717
+ return shader;
718
+ }
719
+
720
+ // Создание программы
721
+ function createProgram(vsSource, fsSource) {
722
+ const vs = compileShader(vsSource, gl.VERTEX_SHADER);
723
+ const fs = compileShader(fsSource, gl.FRAGMENT_SHADER);
724
+ const program = gl.createProgram();
725
+ gl.attachShader(program, vs);
726
+ gl.attachShader(program, fs);
727
+ gl.linkProgram(program);
728
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
729
+ console.error('Program error:', gl.getProgramInfoLog(program));
730
+ return null;
731
+ }
732
+ return program;
733
+ }
734
+
735
+ // Создание текстуры
736
+ function createTexture(width, height) {
737
+ const texture = gl.createTexture();
738
+ gl.bindTexture(gl.TEXTURE_2D, texture);
739
+
740
+ const ext = gl.getExtension('OES_texture_float');
741
+ const extLinear = gl.getExtension('OES_texture_float_linear');
742
+
743
+ if (isWebGL2) {
744
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA32F, width, height, 0, gl.RGBA, gl.FLOAT, null);
745
+ } else if (ext) {
746
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.FLOAT, null);
747
  } else {
748
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
749
+ }
750
+
751
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, extLinear ? gl.LINEAR : gl.NEAREST);
752
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, extLinear ? gl.LINEAR : gl.NEAREST);
753
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
754
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
755
+
756
+ return texture;
757
+ }
758
+
759
+ // Создание framebuffer
760
+ function createFramebuffer(texture) {
761
+ const fb = gl.createFramebuffer();
762
+ gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
763
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
764
+ return fb;
765
+ }
766
+
767
+ // Инициализация
768
+ function init() {
769
+ // Создание буфера квада
770
+ quadBuffer = gl.createBuffer();
771
+ gl.bindBuffer(gl.ARRAY_BUFFER, quadBuffer);
772
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
773
+ -1, -1, 1, -1, -1, 1,
774
+ -1, 1, 1, -1, 1, 1
775
+ ]), gl.STATIC_DRAW);
776
+
777
+ // Создание программ
778
+ waveSimProgram = createProgram(quadVS, waveSimFS);
779
+ dropProgram = createProgram(quadVS, dropFS);
780
+ renderProgram = createProgram(quadVS, renderFS);
781
+
782
+ // Создание текстур и фреймбуферов
783
+ initFramebuffers();
784
+
785
+ resize();
786
+ window.addEventListener('resize', resize);
787
+
788
+ // События мыши и тача
789
+ canvas.addEventListener('mousedown', onPointerDown);
790
+ canvas.addEventListener('mousemove', onPointerMove);
791
+ canvas.addEventListener('mouseup', onPointerUp);
792
+ canvas.addEventListener('touchstart', onTouchStart, { passive: false });
793
+ canvas.addEventListener('touchmove', onTouchMove, { passive: false });
794
+ canvas.addEventListener('touchend', onTouchEnd);
795
+
796
+ // Настройка элементов управления
797
+ setupControls();
798
+
799
+ // Запуск анимации
800
+ requestAnimationFrame(animate);
801
+ }
802
+
803
+ function initFramebuffers() {
804
+ // Удаляем старые
805
+ framebuffers.forEach(fb => gl.deleteFramebuffer(fb));
806
+ textures.forEach(tex => gl.deleteTexture(tex));
807
+
808
+ framebuffers = [];
809
+ textures = [];
810
+
811
+ // Создаем три буфера (prev, curr, next)
812
+ for (let i = 0; i < 3; i++) {
813
+ const tex = createTexture(simWidth, simHeight);
814
+ textures.push(tex);
815
+ framebuffers.push(createFramebuffer(tex));
816
+ }
817
+
818
+ currentFB = 0;
819
+ }
820
+
821
+ function resize() {
822
+ const dpr = Math.min(window.devicePixelRatio, 2);
823
+ canvas.width = window.innerWidth * dpr;
824
+ canvas.height = window.innerHeight * dpr;
825
+ gl.viewport(0, 0, canvas.width, canvas.height);
826
+ }
827
+
828
+ // Обработчики событий
829
+ let isPointerDown = false;
830
+ let lastPointerPos = null;
831
+
832
+ function getPointerPos(e) {
833
+ const rect = canvas.getBoundingClientRect();
834
+ const x = (e.clientX - rect.left) / rect.width;
835
+ const y = 1.0 - (e.clientY - rect.top) / rect.height;
836
+ return { x, y };
837
+ }
838
+
839
+ function onPointerDown(e) {
840
+ isPointerDown = true;
841
+ const pos = getPointerPos(e);
842
+ addDrop(pos.x * simWidth, pos.y * simHeight);
843
+ lastPointerPos = pos;
844
+ }
845
+
846
+ function onPointerMove(e) {
847
+ if (!isPointerDown) return;
848
+ const pos = getPointerPos(e);
849
+ if (lastPointerPos) {
850
+ const dist = Math.sqrt(
851
+ Math.pow(pos.x - lastPointerPos.x, 2) +
852
+ Math.pow(pos.y - lastPointerPos.y, 2)
853
+ );
854
+ if (dist > 0.01) {
855
+ addDrop(pos.x * simWidth, pos.y * simHeight);
856
+ lastPointerPos = pos;
857
  }
858
  }
 
859
  }
860
 
861
+ function onPointerUp() {
862
+ isPointerDown = false;
863
+ lastPointerPos = null;
864
+ }
865
+
866
+ function onTouchStart(e) {
867
+ e.preventDefault();
868
+ for (let touch of e.touches) {
869
+ const pos = getPointerPos(touch);
870
+ addDrop(pos.x * simWidth, pos.y * simHeight);
871
  }
872
+ isPointerDown = true;
873
  }
874
+
875
+ function onTouchMove(e) {
876
+ e.preventDefault();
877
+ for (let touch of e.touches) {
878
+ const pos = getPointerPos(touch);
879
+ addDrop(pos.x * simWidth, pos.y * simHeight);
880
+ }
881
+ }
882
+
883
+ function onTouchEnd(e) {
884
+ if (e.touches.length === 0) {
885
+ isPointerDown = false;
886
+ }
887
+ }
888
+
889
+ // Добавление капли
890
+ function addDrop(x, y) {
891
+ gl.useProgram(dropProgram);
892
+
893
+ gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffers[(currentFB + 1) % 3]);
894
+ gl.viewport(0, 0, simWidth, simHeight);
895
+
896
+ gl.activeTexture(gl.TEXTURE0);
897
+ gl.bindTexture(gl.TEXTURE_2D, textures[(currentFB + 1) % 3]);
898
+ gl.uniform1i(gl.getUniformLocation(dropProgram, 'uState'), 0);
899
+
900
+ gl.uniform2f(gl.getUniformLocation(dropProgram, 'uDropPos'), x, y);
901
+ gl.uniform1f(gl.getUniformLocation(dropProgram, 'uDropRadius'), params.dropRadius);
902
+ gl.uniform1f(gl.getUniformLocation(dropProgram, 'uDropStrength'), params.dropStrength);
903
+ gl.uniform2f(gl.getUniformLocation(dropProgram, 'uResolution'), simWidth, simHeight);
904
+
905
+ drawQuad(dropProgram);
906
+ }
907
+
908
+ // Симуляция волн
909
+ function simulateWaves() {
910
+ gl.useProgram(waveSimProgram);
911
+
912
+ const nextFB = (currentFB + 2) % 3;
913
+ gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffers[nextFB]);
914
+ gl.viewport(0, 0, simWidth, simHeight);
915
+
916
+ gl.activeTexture(gl.TEXTURE0);
917
+ gl.bindTexture(gl.TEXTURE_2D, textures[currentFB]);
918
+ gl.uniform1i(gl.getUniformLocation(waveSimProgram, 'uPrevState'), 0);
919
+
920
+ gl.activeTexture(gl.TEXTURE1);
921
+ gl.bindTexture(gl.TEXTURE_2D, textures[(currentFB + 1) % 3]);
922
+ gl.uniform1i(gl.getUniformLocation(waveSimProgram, 'uCurrState'), 1);
923
+
924
+ gl.uniform2f(gl.getUniformLocation(waveSimProgram, 'uResolution'), simWidth, simHeight);
925
+ gl.uniform1f(gl.getUniformLocation(waveSimProgram, 'uDamping'), params.damping);
926
+ gl.uniform1f(gl.getUniformLocation(waveSimProgram, 'uSpeed'), params.waveSpeed * 0.5);
927
+ gl.uniform1f(gl.getUniformLocation(waveSimProgram, 'uTension'), params.tension);
928
+ gl.uniform1f(gl.getUniformLocation(waveSimProgram, 'uDt'), 1.0 / 60.0);
929
+
930
+ drawQuad(waveSimProgram);
931
+
932
+ currentFB = (currentFB + 1) % 3;
933
+ }
934
+
935
+ // Рендеринг
936
+ let time = 0;
937
+ function render() {
938
+ gl.useProgram(renderProgram);
939
+
940
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
941
+ gl.viewport(0, 0, canvas.width, canvas.height);
942
+
943
+ gl.activeTexture(gl.TEXTURE0);
944
+ gl.bindTexture(gl.TEXTURE_2D, textures[(currentFB + 1) % 3]);
945
+ gl.uniform1i(gl.getUniformLocation(renderProgram, 'uState'), 0);
946
+
947
+ gl.uniform2f(gl.getUniformLocation(renderProgram, 'uResolution'), simWidth, simHeight);
948
+ gl.uniform1f(gl.getUniformLocation(renderProgram, 'uTime'), time);
949
+ gl.uniform3fv(gl.getUniformLocation(renderProgram, 'uWaterColor'), params.waterColor);
950
+ gl.uniform3fv(gl.getUniformLocation(renderProgram, 'uDeepColor'), params.deepColor);
951
+ gl.uniform3fv(gl.getUniformLocation(renderProgram, 'uSkyColor'), params.skyColor);
952
+ gl.uniform3fv(gl.getUniformLocation(renderProgram, 'uFoamColor'), params.foamColor);
953
+ gl.uniform3f(gl.getUniformLocation(renderProgram, 'uLightDir'),
954
+ params.lightX, params.lightY, params.lightZ);
955
+ gl.uniform1f(gl.getUniformLocation(renderProgram, 'uRefraction'), params.refraction);
956
+ gl.uniform1f(gl.getUniformLocation(renderProgram, 'uReflection'), params.reflection);
957
+ gl.uniform1f(gl.getUniformLocation(renderProgram, 'uCaustics'), params.caustics);
958
+ gl.uniform1f(gl.getUniformLocation(renderProgram, 'uBrightness'), params.brightness);
959
+ gl.uniform1f(gl.getUniformLocation(renderProgram, 'uSpecular'), params.specular);
960
+ gl.uniform1f(gl.getUniformLocation(renderProgram, 'uWaterDepth'), params.waterDepth);
961
+ gl.uniform1i(gl.getUniformLocation(renderProgram, 'uHighQuality'), params.highQuality ? 1 : 0);
962
+
963
+ drawQuad(renderProgram);
964
+ }
965
+
966
+ function drawQuad(program) {
967
+ const posLoc = gl.getAttribLocation(program, 'position');
968
+ gl.bindBuffer(gl.ARRAY_BUFFER, quadBuffer);
969
+ gl.enableVertexAttribArray(posLoc);
970
+ gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0);
971
+ gl.drawArrays(gl.TRIANGLES, 0, 6);
972
+ }
973
+
974
+ // FPS счетчик
975
+ let frameCount = 0;
976
+ let lastFpsUpdate = performance.now();
977
+ let fps = 60;
978
+
979
+ function updateFPS() {
980
+ frameCount++;
981
+ const now = performance.now();
982
+ if (now - lastFpsUpdate > 500) {
983
+ fps = Math.round(frameCount * 1000 / (now - lastFpsUpdate));
984
+ document.getElementById('fps').textContent = `FPS: ${fps}`;
985
+ frameCount = 0;
986
+ lastFpsUpdate = now;
987
+ }
988
+ }
989
+
990
+ // Главный цикл
991
+ function animate() {
992
+ time += 0.016;
993
+
994
+ // Автоматические волны (дождь)
995
+ if (params.autoWaves && Math.random() < params.rainFreq) {
996
+ addDrop(
997
+ Math.random() * simWidth,
998
+ Math.random() * simHeight
999
+ );
1000
+ }
1001
+
1002
+ // Ветровые волны
1003
+ if (params.windWaves) {
1004
+ if (Math.random() < 0.3) {
1005
+ const x = Math.random() * simWidth;
1006
+ addDropWeak(x, simHeight * 0.9, params.windStrength);
1007
  }
1008
  }
1009
+
1010
+ // Симуляция (несколько итераций для стабильности)
1011
+ for (let i = 0; i < 2; i++) {
1012
+ simulateWaves();
1013
+ }
1014
+
1015
+ render();
1016
+ updateFPS();
1017
+
1018
+ requestAnimationFrame(animate);
1019
+ }
1020
+
1021
+ function addDropWeak(x, y, strength) {
1022
+ gl.useProgram(dropProgram);
1023
+
1024
+ gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffers[(currentFB + 1) % 3]);
1025
+ gl.viewport(0, 0, simWidth, simHeight);
1026
+
1027
+ gl.activeTexture(gl.TEXTURE0);
1028
+ gl.bindTexture(gl.TEXTURE_2D, textures[(currentFB + 1) % 3]);
1029
+ gl.uniform1i(gl.getUniformLocation(dropProgram, 'uState'), 0);
1030
+
1031
+ gl.uniform2f(gl.getUniformLocation(dropProgram, 'uDropPos'), x, y);
1032
+ gl.uniform1f(gl.getUniformLocation(dropProgram, 'uDropRadius'), 5);
1033
+ gl.uniform1f(gl.getUniformLocation(dropProgram, 'uDropStrength'), strength);
1034
+ gl.uniform2f(gl.getUniformLocation(dropProgram, 'uResolution'), simWidth, simHeight);
1035
+
1036
+ drawQuad(dropProgram);
1037
+ }
1038
+
1039
+ // Настройка элементов управления
1040
+ function setupControls() {
1041
+ const controls = [
1042
+ { id: 'waveSpeed', param: 'waveSpeed', decimals: 1 },
1043
+ { id: 'damping', param: 'damping', decimals: 3 },
1044
+ { id: 'dropRadius', param: 'dropRadius', decimals: 0 },
1045
+ { id: 'dropStrength', param: 'dropStrength', decimals: 1 },
1046
+ { id: 'tension', param: 'tension', decimals: 2 },
1047
+ { id: 'waterDepth', param: 'waterDepth', decimals: 1 },
1048
+ { id: 'refraction', param: 'refraction', decimals: 2 },
1049
+ { id: 'reflection', param: 'reflection', decimals: 2 },
1050
+ { id: 'caustics', param: 'caustics', decimals: 1 },
1051
+ { id: 'brightness', param: 'brightness', decimals: 1 },
1052
+ { id: 'specular', param: 'specular', decimals: 1 },
1053
+ { id: 'lightX', param: 'lightX', decimals: 1 },
1054
+ { id: 'lightY', param: 'lightY', decimals: 1 },
1055
+ { id: 'lightZ', param: 'lightZ', decimals: 1 },
1056
+ { id: 'rainFreq', param: 'rainFreq', decimals: 2 },
1057
+ { id: 'windStrength', param: 'windStrength', decimals: 3 },
1058
+ { id: 'simResolution', param: 'simResolution', decimals: 0 }
1059
+ ];
1060
+
1061
+ controls.forEach(ctrl => {
1062
+ const input = document.getElementById(ctrl.id);
1063
+ const valueSpan = document.getElementById(ctrl.id + 'Val');
1064
+
1065
+ if (input) {
1066
+ input.value = params[ctrl.param];
1067
+ if (valueSpan) {
1068
+ valueSpan.textContent = params[ctrl.param].toFixed(ctrl.decimals);
1069
+ }
1070
+
1071
+ input.addEventListener('input', () => {
1072
+ params[ctrl.param] = parseFloat(input.value);
1073
+ if (valueSpan) {
1074
+ valueSpan.textContent = params[ctrl.param].toFixed(ctrl.decimals);
1075
+ }
1076
+
1077
+ // Изменение разрешения требует перезапуска
1078
+ if (ctrl.id === 'simResolution') {
1079
+ simWidth = params.simResolution;
1080
+ simHeight = params.simResolution;
1081
+ initFramebuffers();
1082
+ }
1083
+ });
1084
+ }
1085
+ });
1086
+
1087
+ // Цвета
1088
+ const colorInputs = [
1089
+ { id: 'waterColor', param: 'waterColor' },
1090
+ { id: 'deepColor', param: 'deepColor' },
1091
+ { id: 'skyColor', param: 'skyColor' },
1092
+ { id: 'foamColor', param: 'foamColor' }
1093
+ ];
1094
+
1095
+ colorInputs.forEach(ctrl => {
1096
+ const input = document.getElementById(ctrl.id);
1097
+ if (input) {
1098
+ input.addEventListener('input', () => {
1099
+ params[ctrl.param] = hexToRgb(input.value);
1100
+ });
1101
+ }
1102
+ });
1103
+
1104
+ // Чекбоксы
1105
+ document.getElementById('autoWaves').addEventListener('change', (e) => {
1106
+ params.autoWaves = e.target.checked;
1107
+ });
1108
+
1109
+ document.getElementById('windWaves').addEventListener('change', (e) => {
1110
+ params.windWaves = e.target.checked;
1111
+ });
1112
+
1113
+ document.getElementById('highQuality').addEventListener('change', (e) => {
1114
+ params.highQuality = e.target.checked;
1115
+ });
1116
+ }
1117
+
1118
+ function hexToRgb(hex) {
1119
+ const r = parseInt(hex.slice(1, 3), 16) / 255;
1120
+ const g = parseInt(hex.slice(3, 5), 16) / 255;
1121
+ const b = parseInt(hex.slice(5, 7), 16) / 255;
1122
+ return [r, g, b];
1123
+ }
1124
+
1125
+ function rgbToHex(rgb) {
1126
+ const r = Math.round(rgb[0] * 255).toString(16).padStart(2, '0');
1127
+ const g = Math.round(rgb[1] * 255).toString(16).padStart(2, '0');
1128
+ const b = Math.round(rgb[2] * 255).toString(16).padStart(2, '0');
1129
+ return `#${r}${g}${b}`;
1130
+ }
1131
+
1132
+ // Пресеты
1133
+ function applyPreset(preset) {
1134
+ const presets = {
1135
+ calm: {
1136
+ waveSpeed: 0.5, damping: 0.998, dropRadius: 20, dropStrength: 0.3,
1137
+ tension: 0.15, waterDepth: 0.6, refraction: 0.2, reflection: 0.5,
1138
+ caustics: 0.5, brightness: 1.0, specular: 1.0,
1139
+ waterColor: [0.1, 0.4, 0.6], deepColor: [0.02, 0.1, 0.15],
1140
+ skyColor: [0.9, 0.7, 0.5], autoWaves: false, rainFreq: 0.01
1141
+ },
1142
+ storm: {
1143
+ waveSpeed: 2.0, damping: 0.99, dropRadius: 10, dropStrength: 0.8,
1144
+ tension: 0.05, waterDepth: 1.2, refraction: 0.4, reflection: 0.3,
1145
+ caustics: 1.5, brightness: 0.8, specular: 2.0,
1146
+ waterColor: [0.05, 0.15, 0.25], deepColor: [0.01, 0.03, 0.08],
1147
+ skyColor: [0.3, 0.35, 0.4], autoWaves: true, rainFreq: 0.2
1148
+ },
1149
+ pool: {
1150
+ waveSpeed: 1.2, damping: 0.997, dropRadius: 15, dropStrength: 0.4,
1151
+ tension: 0.2, waterDepth: 0.5, refraction: 0.35, reflection: 0.6,
1152
+ caustics: 2.0, brightness: 1.2, specular: 1.5,
1153
+ waterColor: [0.2, 0.6, 0.8], deepColor: [0.1, 0.3, 0.5],
1154
+ skyColor: [0.5, 0.8, 1.0], autoWaves: false, rainFreq: 0.02
1155
+ },
1156
+ ocean: {
1157
+ waveSpeed: 1.5, damping: 0.994, dropRadius: 25, dropStrength: 0.6,
1158
+ tension: 0.08, waterDepth: 1.5, refraction: 0.25, reflection: 0.45,
1159
+ caustics: 1.0, brightness: 1.0, specular: 1.2,
1160
+ waterColor: [0.0, 0.3, 0.5], deepColor: [0.0, 0.05, 0.15],
1161
+ skyColor: [0.6, 0.8, 0.95], autoWaves: true, rainFreq: 0.05,
1162
+ windWaves: true, windStrength: 0.005
1163
+ },
1164
+ toxic: {
1165
+ waveSpeed: 0.8, damping: 0.996, dropRadius: 18, dropStrength: 0.5,
1166
+ tension: 0.12, waterDepth: 0.7, refraction: 0.3, reflection: 0.4,
1167
+ caustics: 1.5, brightness: 1.3, specular: 1.0,
1168
+ waterColor: [0.2, 0.8, 0.1], deepColor: [0.05, 0.2, 0.0],
1169
+ skyColor: [0.4, 0.5, 0.2], autoWaves: true, rainFreq: 0.03
1170
+ },
1171
+ lava: {
1172
+ waveSpeed: 0.4, damping: 0.992, dropRadius: 30, dropStrength: 0.7,
1173
+ tension: 0.02, waterDepth: 1.0, refraction: 0.15, reflection: 0.2,
1174
+ caustics: 0.3, brightness: 1.5, specular: 0.5,
1175
+ waterColor: [1.0, 0.3, 0.0], deepColor: [0.3, 0.0, 0.0],
1176
+ skyColor: [0.2, 0.1, 0.05], autoWaves: true, rainFreq: 0.08
1177
+ }
1178
+ };
1179
+
1180
+ const p = presets[preset];
1181
+ if (!p) return;
1182
+
1183
+ Object.assign(params, p);
1184
+
1185
+ // Обновляем UI
1186
+ updateControlsUI();
1187
+ }
1188
+
1189
+ function updateControlsUI() {
1190
+ document.getElementById('waveSpeed').value = params.waveSpeed;
1191
+ document.getElementById('waveSpeedVal').textContent = params.waveSpeed.toFixed(1);
1192
+
1193
+ document.getElementById('damping').value = params.damping;
1194
+ document.getElementById('dampingVal').textContent = params.damping.toFixed(3);
1195
+
1196
+ document.getElementById('dropRadius').value = params.dropRadius;
1197
+ document.getElementById('dropRadiusVal').textContent = params.dropRadius.toFixed(0);
1198
+
1199
+ document.getElementById('dropStrength').value = params.dropStrength;
1200
+ document.getElementById('dropStrengthVal').textContent = params.dropStrength.toFixed(1);
1201
+
1202
+ document.getElementById('tension').value = params.tension;
1203
+ document.getElementById('tensionVal').textContent = params.tension.toFixed(2);
1204
+
1205
+ document.getElementById('waterDepth').value = params.waterDepth;
1206
+ document.getElementById('waterDepthVal').textContent = params.waterDepth.toFixed(1);
1207
+
1208
+ document.getElementById('refraction').value = params.refraction;
1209
+ document.getElementById('refractionVal').textContent = params.refraction.toFixed(2);
1210
+
1211
+ document.getElementById('reflection').value = params.reflection;
1212
+ document.getElementById('reflectionVal').textContent = params.reflection.toFixed(2);
1213
+
1214
+ document.getElementById('caustics').value = params.caustics;
1215
+ document.getElementById('causticsVal').textContent = params.caustics.toFixed(1);
1216
+
1217
+ document.getElementById('brightness').value = params.brightness;
1218
+ document.getElementById('brightnessVal').textContent = params.brightness.toFixed(1);
1219
+
1220
+ document.getElementById('specular').value = params.specular;
1221
+ document.getElementById('specularVal').textContent = params.specular.toFixed(1);
1222
+
1223
+ document.getElementById('rainFreq').value = params.rainFreq;
1224
+ document.getElementById('rainFreqVal').textContent = params.rainFreq.toFixed(2);
1225
+
1226
+ document.getElementById('autoWaves').checked = params.autoWaves;
1227
+ document.getElementById('windWaves').checked = params.windWaves || false;
1228
+
1229
+ document.getElementById('waterColor').value = rgbToHex(params.waterColor);
1230
+ document.getElementById('deepColor').value = rgbToHex(params.deepColor);
1231
+ document.getElementById('skyColor').value = rgbToHex(params.skyColor);
1232
  }
1233
+
1234
+ function resetWater() {
1235
+ initFramebuffers();
1236
+ }
1237
+
1238
+ function toggleControls() {
1239
+ document.getElementById('controls').classList.toggle('hidden');
1240
+ }
1241
+
1242
+ // Запуск
1243
+ init();
1244
+ </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1245
  </body>
1246
  </html>