Files changed (1) hide show
  1. index.html +308 -1163
index.html CHANGED
@@ -2,1245 +2,390 @@
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>
 
2
  <html lang="ru">
3
  <head>
4
  <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>LMArena WebView + Prompt Generator</title>
7
  <style>
8
+ body, html {
9
  margin: 0;
10
  padding: 0;
11
+ height: 100%;
12
+ width: 100%;
 
 
13
  overflow: hidden;
14
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
 
 
15
  }
16
 
17
+ /* Стиль для WebView (iframe) */
18
+ iframe {
19
+ width: 100%;
20
+ height: 100%;
21
+ border: none;
22
  display: block;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  }
24
 
25
+ /* Плавающая кнопка справа */
26
+ .fab-btn {
 
 
 
27
  position: fixed;
28
+ right: 20px;
29
+ top: 50%;
30
+ transform: translateY(-50%);
31
+ width: 60px;
32
+ height: 60px;
33
+ background-color: #4CAF50;
34
+ color: white;
35
+ border-radius: 50%;
36
+ border: none;
37
+ box-shadow: 0 4px 8px rgba(0,0,0,0.3);
38
  cursor: pointer;
39
+ z-index: 1000;
40
+ font-size: 24px;
 
 
 
 
41
  display: flex;
42
  align-items: center;
43
  justify-content: center;
44
+ transition: transform 0.2s, background-color 0.2s;
45
  }
46
 
47
+ .fab-btn:hover {
48
+ transform: translateY(-50%) scale(1.1);
49
+ background-color: #45a049;
 
50
  }
51
 
52
+ /* Модальное окно (фон) */
53
+ .modal-overlay {
54
+ display: none;
55
+ position: fixed;
56
+ top: 0;
57
+ left: 0;
58
+ width: 100%;
59
+ height: 100%;
60
+ background-color: rgba(0, 0, 0, 0.7);
61
+ z-index: 2000;
62
+ justify-content: center;
63
+ align-items: center;
64
  }
65
 
66
+ /* Контент модального окна */
67
+ .modal-content {
68
+ background-color: white;
69
+ padding: 25px;
70
+ border-radius: 12px;
71
+ width: 90%;
72
+ max-width: 500px;
73
+ max-height: 90vh;
74
+ overflow-y: auto;
75
+ box-shadow: 0 5px 15px rgba(0,0,0,0.3);
76
+ position: relative;
77
  }
78
 
79
+ .modal-header {
80
  display: flex;
81
+ justify-content: space-between;
82
  align-items: center;
83
+ margin-bottom: 20px;
84
+ border-bottom: 1px solid #eee;
85
+ padding-bottom: 10px;
86
  }
87
 
88
+ .modal-header h2 {
89
+ margin: 0;
90
+ font-size: 1.2rem;
91
+ color: #333;
92
  }
93
 
94
+ .close-btn {
95
+ background: none;
96
+ border: none;
97
+ font-size: 28px;
98
+ cursor: pointer;
99
+ color: #aaa;
 
100
  }
101
 
102
+ .close-btn:hover {
103
+ color: #333;
 
 
 
 
 
104
  }
105
 
106
+ /* Форма */
107
+ .form-group {
108
+ margin-bottom: 15px;
 
 
 
109
  }
110
 
111
+ .form-group label {
112
+ display: block;
113
+ margin-bottom: 5px;
114
+ font-weight: 600;
115
+ font-size: 0.9rem;
116
+ color: #555;
117
  }
118
 
119
+ .form-group select, .form-group textarea {
120
  width: 100%;
121
  padding: 10px;
122
+ border: 1px solid #ccc;
123
+ border-radius: 6px;
124
+ font-size: 1rem;
125
+ box-sizing: border-box; /* Чтобы padding не ломал ширину */
 
 
 
 
126
  }
127
 
128
+ .form-group textarea {
129
+ resize: vertical;
130
+ min-height: 60px;
131
  }
132
 
133
+ /* Кнопка "Готово" */
134
+ .submit-btn {
135
+ width: 100%;
136
+ padding: 12px;
137
+ background-color: #2196F3;
138
+ color: white;
 
 
 
139
  border: none;
140
+ border-radius: 6px;
141
+ font-size: 1rem;
142
  cursor: pointer;
 
 
 
 
 
 
 
 
 
143
  margin-top: 10px;
144
+ font-weight: bold;
145
  }
146
 
147
+ .submit-btn:hover {
148
+ background-color: #0b7dda;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  }
150
 
151
+ /* Адаптив для маленьких экранов */
152
+ @media (max-width: 600px) {
153
+ .modal-content {
154
+ width: 95%;
155
+ padding: 15px;
 
 
 
 
 
 
 
 
156
  }
157
+ .fab-btn {
158
+ width: 50px;
159
+ height: 50px;
160
+ font-size: 20px;
161
  }
162
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
  </style>
164
  </head>
165
  <body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
 
167
+ <!-- Webview Сайта -->
168
+ <iframe src="https://lmarena.ai/ru?chat-modality=image&mode=direct" allow="clipboard-write"></iframe>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
 
170
+ <!-- Кнопка вызова генератора -->
171
+ <button class="fab-btn" onclick="openModal()" title="Генератор промптов">
172
+ 🎨
173
+ </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
 
175
+ <!-- Модальное окно -->
176
+ <div class="modal-overlay" id="promptModal">
177
+ <div class="modal-content">
178
+ <div class="modal-header">
179
+ <h2>Параметры Модели</h2>
180
+ <button class="close-btn" onclick="closeModal()">&times;</button>
 
 
 
 
181
  </div>
 
182
 
183
+ <form id="promptForm">
184
+ <!-- Пол -->
185
+ <div class="form-group">
186
+ <label>Пол</label>
187
+ <select id="gender">
188
+ <option value="Male">Мужчина</option>
189
+ <option value="Female">Женщина</option>
190
+ </select>
191
+ </div>
192
+
193
+ <!-- Возраст -->
194
+ <div class="form-group">
195
+ <label>Возраст</label>
196
+ <select id="age">
197
+ <option value="Baby (0-2 years)">Младенец (0-2)</option>
198
+ <option value="Child (3-12 years)">Ребенок (3-12)</option>
199
+ <option value="Teenager (13-19 years)">Подросток (13-19)</option>
200
+ <option value="Young Adult (20-29 years)">Молодой (20-29)</option>
201
+ <option value="Adult (30-39 years)">Взрослый (30-39)</option>
202
+ <option value="Middle-aged (40-49 years)">Средних лет (40-49)</option>
203
+ <option value="Senior (50-59 years)">Пожилой (50-59)</option>
204
+ <option value="Elderly (60-69 years)">Старый (60-69)</option>
205
+ <option value="Very Old (70+ years)">Глубокая старость (70+)</option>
206
+ </select>
207
+ </div>
208
+
209
+ <!-- Национальность -->
210
+ <div class="form-group">
211
+ <label>Национальность</label>
212
+ <select id="nationality">
213
+ <option value="European/Caucasian">Европейская</option>
214
+ <option value="African/Black">Африканская</option>
215
+ <option value="Asian">Азиатская</option>
216
+ <option value="Latino/Hispanic">Латиноамериканская</option>
217
+ <option value="Indian">Индийская</option>
218
+ <option value="Middle Eastern">Ближневосточная</option>
219
+ <option value="Slavic">Славянская</option>
220
+ <option value="Nordic">Скандинавская</option>
221
+ <option value="Mixed race">Смешанная</option>
222
+ </select>
223
+ </div>
224
+
225
+ <!-- Цвет волос -->
226
+ <div class="form-group">
227
+ <label>Цвет волос</label>
228
+ <select id="hairColor">
229
+ <option value="Blonde">Блонд</option>
230
+ <option value="Brunette">Брюнет</option>
231
+ <option value="Black">Черные</option>
232
+ <option value="Red">Рыжие</option>
233
+ <option value="Grey">Седые</option>
234
+ <option value="White">Белые</option>
235
+ <option value="Dyed Blue">Крашеные (Синий)</option>
236
+ <option value="Dyed Pink">Крашеные (Розовый)</option>
237
+ <option value="Bald">Лысый</option>
238
+ </select>
239
+ </div>
240
+
241
+ <!-- Цвет глаз -->
242
+ <div class="form-group">
243
+ <label>Цвет глаз</label>
244
+ <select id="eyeColor">
245
+ <option value="Blue">Голубые</option>
246
+ <option value="Green">Зеленые</option>
247
+ <option value="Brown">Карие</option>
248
+ <option value="Hazel">Ореховые</option>
249
+ <option value="Grey">Серые</option>
250
+ <option value="Heterochromia">Разные глаза (Гетерохромия)</option>
251
+ </select>
252
+ </div>
253
+
254
+ <!-- Прическа -->
255
+ <div class="form-group">
256
+ <label>Прическа</label>
257
+ <select id="hairstyle">
258
+ <option value="Long straight">Длинные прямые</option>
259
+ <option value="Short cut">Короткая стрижка</option>
260
+ <option value="Bob cut">Каре</option>
261
+ <option value="Curly">Кудрявые</option>
262
+ <option value="Wavy">Волнистые</option>
263
+ <option value="Ponytail">Хвост</option>
264
+ <option value="Bun">Пучок</option>
265
+ <option value="Buzz cut">Ежик (под машинку)</option>
266
+ <option value="Dreadlocks">Дреды</option>
267
+ <option value="Messy">Растрепанные</option>
268
+ </select>
269
+ </div>
270
+
271
+ <!-- Фигура -->
272
+ <div class="form-group">
273
+ <label>Фигура</label>
274
+ <select id="bodyType">
275
+ <option value="Slim">Стройная/Худая</option>
276
+ <option value="Athletic">Атлетичная/Спортивная</option>
277
+ <option value="Muscular">Мускулистая</option>
278
+ <option value="Curvy">С формами (Curvy)</option>
279
+ <option value="Average">Обычная</option>
280
+ <option value="Plus-size">Плюс-сайз</option>
281
+ <option value="Skinny">Очень худая</option>
282
+ </select>
283
+ </div>
284
+
285
+ <!-- Стиль фото -->
286
+ <div class="form-group">
287
+ <label>Стиль фотографии</label>
288
+ <select id="style">
289
+ <option value="Professional Studio Portrait, high key lighting">Студийный портрет (проф)</option>
290
+ <option value="Creative Artistic, colorful lighting">Творческий/Арт</option>
291
+ <option value="Cinematic, movie still, dramatic lighting">Кинематографичный</option>
292
+ <option value="Street Photography, natural light">Уличная фотография</option>
293
+ <option value="Cyberpunk, neon lights">Киберпанк (Неон)</option>
294
+ <option value="Black and White, high contrast">Чёрно-белое</option>
295
+ <option value="Fashion Editorial">Фэшн (Журнальный)</option>
296
+ <option value="Vintage Polaroid">Винтаж (Полароид)</option>
297
+ <option value="Fantasy, ethereal">Фэнтези</option>
298
+ <option value="Corporate Headshot">Деловой портрет</option>
299
+ </select>
300
+ </div>
301
+
302
+ <!-- Доп пожелания -->
303
+ <div class="form-group">
304
+ <label>Дополнительные пожелания (напр. одежда, фон)</label>
305
+ <textarea id="additional" placeholder="Например: в деловом костюме, на фоне ночного города..."></textarea>
306
+ </div>
307
+
308
+ <button type="button" class="submit-btn" onclick="generateAndCopy()">ГОТОВО</button>
309
+ </form>
310
  </div>
311
  </div>
312
 
 
 
 
313
  <script>
314
+ // Функции открытия и закрытия модального окна
315
+ function openModal() {
316
+ document.getElementById('promptModal').style.display = 'flex';
 
 
 
317
  }
318
 
319
+ function closeModal() {
320
+ document.getElementById('promptModal').style.display = 'none';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
321
  }
322
 
323
+ // Закрытие по клику вне окна
324
+ window.onclick = function(event) {
325
+ const modal = document.getElementById('promptModal');
326
+ if (event.target == modal) {
327
+ closeModal();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
328
  }
 
 
 
 
 
329
  }
330
 
331
+ // Основная логика генерации
332
+ async function generateAndCopy() {
333
+ // Сбор данных из формы
334
+ const gender = document.getElementById('gender').value;
335
+ const age = document.getElementById('age').value;
336
+ const nationality = document.getElementById('nationality').value;
337
+ const hairColor = document.getElementById('hairColor').value;
338
+ const eyeColor = document.getElementById('eyeColor').value;
339
+ const hairstyle = document.getElementById('hairstyle').value;
340
+ const bodyType = document.getElementById('bodyType').value;
341
+ const style = document.getElementById('style').value;
342
+ // Перевод пользовательского ввода (упрощенный, так как ввод на русском, но промпт нужен на английском)
343
+ // В идеале здесь нужен API переводчик, но мы просто добавим текст как есть,
344
+ // так как современные модели понимают смешанный контекст или транслитерацию.
345
+ // Но лучше всего пометить это как "custom_notes".
346
+ const additional = document.getElementById('additional').value;
347
+
348
+ // Формирование JSON объекта
349
+ const promptObject = {
350
+ "photo_request": {
351
+ "subject": {
352
+ "gender": gender,
353
+ "age_category": age,
354
+ "ethnicity": nationality,
355
+ "appearance": {
356
+ "hair_color": hairColor,
357
+ "eye_color": eyeColor,
358
+ "hairstyle": hairstyle,
359
+ "body_type": bodyType
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
360
  }
361
+ },
362
+ "photography_style": style,
363
+ "technical_settings": "8k resolution, photorealistic, highly detailed, professional photography, depth of field",
364
+ "additional_notes": additional ? additional : "None"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
365
  }
366
  };
367
 
368
+ // Преобразование в строку
369
+ const jsonString = JSON.stringify(promptObject, null, 2);
370
+
371
+ // Копирование в буфер обмена
372
+ try {
373
+ await navigator.clipboard.writeText(jsonString);
374
+ alert("JSON промпт скопирован в буфер обмена!");
375
+ closeModal();
376
+ } catch (err) {
377
+ console.error('Ошибка копирования: ', err);
378
+ // Фоллбэк для старых браузеров или если нет https
379
+ const textArea = document.createElement("textarea");
380
+ textArea.value = jsonString;
381
+ document.body.appendChild(textArea);
382
+ textArea.select();
383
+ document.execCommand("Copy");
384
+ textArea.remove();
385
+ alert("JSON промпт скопирован в буфер обмена!");
386
+ closeModal();
387
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
388
  }
 
 
 
389
  </script>
390
  </body>
391
  </html>