One commited on
Commit
f385e6d
·
verified ·
1 Parent(s): 63e0092

Upload index.html

Browse files
Files changed (1) hide show
  1. index.html +721 -19
index.html CHANGED
@@ -1,19 +1,721 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Digital Aquarium</title>
7
+ <style>
8
+ html, body { margin:0; padding:0; height:100%; overflow:hidden; background:#02121f; }
9
+ #tank { display:block; width:100vw; height:100vh; }
10
+ .glass {
11
+ position:fixed; inset:0; pointer-events:none; z-index:2;
12
+ background:
13
+ linear-gradient(115deg, rgba(255,255,255,0.07) 0%, rgba(255,255,255,0) 30%),
14
+ linear-gradient(295deg, rgba(255,255,255,0.05) 0%, rgba(255,255,255,0) 25%);
15
+ mix-blend-mode: screen;
16
+ }
17
+ </style>
18
+ </head>
19
+ <body>
20
+ <canvas id="tank"></canvas>
21
+ <div class="glass"></div>
22
+ <script>
23
+ (() => {
24
+ "use strict";
25
+ const canvas = document.getElementById("tank");
26
+ const ctx = canvas.getContext("2d");
27
+ let W = 0, H = 0;
28
+
29
+ const rand = (a, b) => a + Math.random() * (b - a);
30
+ const pick = arr => arr[(Math.random() * arr.length) | 0];
31
+
32
+ function lerpAngle(a, b, t) {
33
+ let d = b - a;
34
+ while (d > Math.PI) d -= 2 * Math.PI;
35
+ while (d < -Math.PI) d += 2 * Math.PI;
36
+ return a + d * t;
37
+ }
38
+
39
+ const HUES = [8, 35, 46, 172, 195, 215, 262, 320];
40
+ let fishes = [], bubbles = [], seaweeds = [], motes = [], rays = [], rocks = [], grass = [];
41
+ let plantPoints = []; // collision points along every stalk/branch, rebuilt each frame
42
+ let sandHeights = [], starfish = null;
43
+
44
+ class Fish {
45
+ constructor() {
46
+ this.size = rand(15, 42);
47
+ this.speed = rand(0.55, 1.7) * (this.size / 30 + 0.5);
48
+ this.x = rand(60, W - 60);
49
+ this.y = rand(H * 0.15, H * 0.72);
50
+ this.angle = Math.random() < 0.5 ? rand(-0.4, 0.4) : Math.PI + rand(-0.4, 0.4);
51
+ this.hue = pick(HUES);
52
+ this.phase = rand(0, Math.PI * 2);
53
+
54
+ // Wander steering (Reynolds-style): a circle ahead of the fish whose target point jitters
55
+ this.wanderTheta = rand(0, Math.PI * 2);
56
+ this.wanderRadius = rand(35, 80);
57
+ this.wanderDist = rand(90, 170);
58
+ this.maxTurn = rand(1.4, 2.2); // max rad/s — organic turns, no orbiting
59
+
60
+ // Behavior states: fish don't swim non-stop — they cruise, hover, and rest.
61
+ this.behavior = 'swim'; // 'swim' | 'hover' | 'pause'
62
+ this.behaviorTimer = rand(4, 11); // seconds until it may change behavior
63
+ this.speedFactor = 1; // current speed multiplier (eased toward target)
64
+ this.targetSpeedFactor = 1; // where speedFactor is easing toward
65
+
66
+ // Occasionally puffs a tiny cluster of bubbles from its mouth
67
+ this.bubbleTimer = rand(4, 16); // seconds until the next bubble puff
68
+ }
69
+ update(dt) {
70
+ // Behavior state machine — occasionally hover or come to near-rest
71
+ this.behaviorTimer -= dt;
72
+ if (this.behaviorTimer <= 0) {
73
+ const r = Math.random();
74
+ if (r < 0.45) {
75
+ this.behavior = 'swim';
76
+ this.targetSpeedFactor = 1;
77
+ this.behaviorTimer = rand(4, 11);
78
+ } else if (r < 0.74) {
79
+ this.behavior = 'hover';
80
+ this.targetSpeedFactor = rand(0.25, 0.45); // drift slowly in place
81
+ this.behaviorTimer = rand(2, 6);
82
+ } else {
83
+ this.behavior = 'pause';
84
+ this.targetSpeedFactor = rand(0.03, 0.10); // nearly stopped, just drifting
85
+ this.behaviorTimer = rand(1.5, 4);
86
+ }
87
+ }
88
+ // Ease speed toward the target so speed-ups/slow-downs are smooth, not abrupt
89
+ this.speedFactor += (this.targetSpeedFactor - this.speedFactor) * Math.min(1, dt * 2.2);
90
+
91
+ // Occasionally release a small cluster of bubbles from the mouth
92
+ this.bubbleTimer -= dt;
93
+ if (this.bubbleTimer <= 0) {
94
+ const mx = this.x + Math.cos(this.angle) * this.size * 0.62;
95
+ const my = this.y + Math.sin(this.angle) * this.size * 0.62;
96
+ const n = 2 + ((Math.random() * 3) | 0); // 2..4 bubbles, a tiny cluster
97
+ for (let i = 0; i < n; i++) bubbles.push(new Bubble(false, mx + rand(-3, 3), my + rand(-3, 3)));
98
+ this.bubbleTimer = rand(8, 22); // wait a while before the next puff
99
+ }
100
+
101
+ // Jitter the wander target point around its circle
102
+ this.wanderTheta += rand(-1, 1) * dt * 3.2;
103
+
104
+ // Target sits ahead of the fish (in heading direction), offset by wanderTheta.
105
+ // Steering toward it yields smooth meandering and genuine direction changes.
106
+ const wx = this.x + Math.cos(this.angle) * this.wanderDist + Math.cos(this.wanderTheta) * this.wanderRadius;
107
+ const wy = this.y + Math.sin(this.angle) * this.wanderDist + Math.sin(this.wanderTheta) * this.wanderRadius;
108
+
109
+ let desired = Math.atan2(wy - this.y, wx - this.x);
110
+
111
+ // Soft boundary steering: only kicks in near the actual glass, so fish roam
112
+ // freely across almost the whole tank (including far left and right)
113
+ let pull = 0;
114
+ const mX = W * 0.13, mTop = H * 0.10, mBot = H * 0.16;
115
+ if (this.x < mX) pull = Math.max(pull, 1 - this.x / mX);
116
+ else if (this.x > W - mX) pull = Math.max(pull, 1 - (W - this.x) / mX);
117
+ if (this.y < mTop) pull = Math.max(pull, 1 - this.y / mTop);
118
+ else if (this.y > H - mBot) pull = Math.max(pull, 1 - (H - this.y) / mBot);
119
+ if (pull > 0) {
120
+ const cx = W / 2, cy = H * 0.45;
121
+ desired = lerpAngle(desired, Math.atan2(cy - this.y, cx - this.x), Math.min(1, pull * 1.5));
122
+ }
123
+
124
+ // Floor steering: as a fish nears the sand bed it is steered upward,
125
+ // so it skims above the ground line instead of diving under it
126
+ const floorY = sandAt(this.x) - this.size * 0.35;
127
+ const fZone = H * 0.14;
128
+ if (this.y > floorY - fZone) {
129
+ const fp = Math.min(1, (this.y - (floorY - fZone)) / fZone);
130
+ desired = lerpAngle(desired, Math.atan2(H * 0.45 - this.y, W / 2 - this.x), fp * 1.2);
131
+ }
132
+
133
+ // Limit turn rate so direction changes are smooth and fish-like
134
+ let d = desired - this.angle;
135
+ while (d > Math.PI) d -= 2 * Math.PI;
136
+ while (d < -Math.PI) d += 2 * Math.PI;
137
+ const maxStep = this.maxTurn * dt;
138
+ this.angle += Math.max(-maxStep, Math.min(maxStep, d));
139
+
140
+ const sp = this.speed * this.speedFactor * 60 * dt;
141
+ this.x += Math.cos(this.angle) * sp;
142
+ this.y += Math.sin(this.angle) * sp;
143
+
144
+ // NOTE: fish do NOT collide with plants — they plough straight through.
145
+ // The seaweed bends out of the way instead (see updatePlants).
146
+
147
+ // Hard safety clamp — never below the ground line (sand surface)
148
+ this.x = Math.max(20, Math.min(W - 20, this.x));
149
+ this.y = Math.max(20, Math.min(sandAt(this.x) - this.size * 0.35, this.y));
150
+
151
+ // Tail beat follows actual speed — a resting fish barely moves its tail
152
+ const effSpeed = this.speed * this.speedFactor;
153
+ this.phase += dt * (4 + effSpeed * 3);
154
+ }
155
+ draw(c) {
156
+ c.save();
157
+ c.translate(this.x, this.y);
158
+ c.rotate(this.angle);
159
+ const s = this.size;
160
+ const sway = Math.sin(this.phase) * s * 0.28;
161
+
162
+ // tail fin
163
+ c.fillStyle = `hsla(${this.hue},75%,52%,0.92)`;
164
+ c.beginPath();
165
+ c.moveTo(-s * 0.5, 0);
166
+ c.quadraticCurveTo(-s * 1.0, -s * 0.35 + sway, -s * 1.28, -s * 0.48 + sway);
167
+ c.lineTo(-s * 0.82, sway * 0.5);
168
+ c.lineTo(-s * 1.28, s * 0.48 + sway);
169
+ c.quadraticCurveTo(-s * 1.0, s * 0.35 + sway, -s * 0.5, 0);
170
+ c.fill();
171
+
172
+ // body
173
+ const g = c.createLinearGradient(0, -s * 0.5, 0, s * 0.5);
174
+ g.addColorStop(0, `hsl(${this.hue},82%,64%)`);
175
+ g.addColorStop(1, `hsl(${this.hue},70%,44%)`);
176
+ c.fillStyle = g;
177
+ c.beginPath();
178
+ c.ellipse(0, 0, s, s * 0.42, 0, 0, Math.PI * 2);
179
+ c.fill();
180
+
181
+ // belly highlight
182
+ c.fillStyle = "rgba(255,255,255,0.16)";
183
+ c.beginPath();
184
+ c.ellipse(0, s * 0.14, s * 0.82, s * 0.24, 0, 0, Math.PI * 2);
185
+ c.fill();
186
+
187
+ // dorsal fin
188
+ c.fillStyle = `hsla(${this.hue},80%,50%,0.9)`;
189
+ c.beginPath();
190
+ c.moveTo(-s * 0.15, -s * 0.36);
191
+ c.quadraticCurveTo(s * 0.05, -s * 0.78, s * 0.38, -s * 0.4);
192
+ c.closePath();
193
+ c.fill();
194
+
195
+ // pectoral fin (flapping)
196
+ const flap = Math.sin(this.phase * 1.3) * 0.45;
197
+ c.save();
198
+ c.translate(s * 0.05, s * 0.12);
199
+ c.rotate(0.7 + flap);
200
+ c.fillStyle = `hsla(${this.hue},85%,66%,0.8)`;
201
+ c.beginPath();
202
+ c.ellipse(0, 0, s * 0.3, s * 0.14, 0, 0, Math.PI * 2);
203
+ c.fill();
204
+ c.restore();
205
+
206
+ // eye
207
+ c.fillStyle = "#fff";
208
+ c.beginPath();
209
+ c.arc(s * 0.58, -s * 0.1, s * 0.11, 0, Math.PI * 2);
210
+ c.fill();
211
+ c.fillStyle = "#0d1420";
212
+ c.beginPath();
213
+ c.arc(s * 0.61, -s * 0.1, s * 0.055, 0, Math.PI * 2);
214
+ c.fill();
215
+
216
+ c.restore();
217
+ }
218
+ }
219
+
220
+ class Bubble {
221
+ constructor(init, x, y) {
222
+ if (x !== undefined) { // spawned from a fish's mouth: tiny, one-shot
223
+ this.x = x; this.y = y;
224
+ this.r = rand(1.4, 3.2);
225
+ this.speed = rand(45, 85);
226
+ this.p = rand(0, Math.PI * 2);
227
+ this.isMouth = true;
228
+ } else {
229
+ this.reset(init);
230
+ }
231
+ }
232
+ reset(init) {
233
+ this.x = rand(0, W);
234
+ this.y = init ? rand(H * 0.3, H) : H + rand(10, 80);
235
+ this.r = rand(2, 7);
236
+ this.speed = rand(35, 90);
237
+ this.p = rand(0, Math.PI * 2);
238
+ this.isMouth = false;
239
+ }
240
+ update(dt, t) {
241
+ this.y -= this.speed * dt;
242
+ this.x += Math.sin(t * 1.6 + this.p) * 22 * dt;
243
+ if (this.y < -12) { if (this.isMouth) this.dead = true; else this.reset(false); }
244
+ }
245
+ draw(c) {
246
+ c.lineWidth = 1;
247
+ c.strokeStyle = "rgba(255,255,255,0.35)";
248
+ c.fillStyle = "rgba(255,255,255,0.08)";
249
+ c.beginPath();
250
+ c.arc(this.x, this.y, this.r, 0, Math.PI * 2);
251
+ c.fill();
252
+ c.stroke();
253
+ c.fillStyle = "rgba(255,255,255,0.45)";
254
+ c.beginPath();
255
+ c.arc(this.x - this.r * 0.3, this.y - this.r * 0.3, this.r * 0.25, 0, Math.PI * 2);
256
+ c.fill();
257
+ }
258
+ }
259
+
260
+ function sandAt(x) {
261
+ const i = Math.max(0, Math.min(sandHeights.length - 2, x / 40));
262
+ const f = (x / 40) - Math.floor(i);
263
+ return sandHeights[Math.floor(i)] * (1 - f) + sandHeights[Math.floor(i) + 1] * f;
264
+ }
265
+
266
+ function buildScene() {
267
+ fishes = [];
268
+ bubbles = [];
269
+ seaweeds = [];
270
+ motes = [];
271
+ rays = [];
272
+ rocks = [];
273
+ sandHeights = [];
274
+
275
+ const sandTop = H - Math.max(60, H * 0.12);
276
+ for (let x = 0; x <= W + 40; x += 40) sandHeights.push(sandTop + rand(-8, 14));
277
+
278
+ // plants grow in bundles of stalks sharing a base patch; each stalk sprouts side branches
279
+ const nClusters = Math.max(6, Math.floor(W / 150));
280
+ for (let i = 0; i < nClusters; i++) {
281
+ const cx = rand(45, W - 45);
282
+ const clusterHue = rand(95, 145);
283
+ const nStalks = 3 + ((Math.random() * 4) | 0); // 3..6 stalks per bundle
284
+ for (let j = 0; j < nStalks; j++) {
285
+ const st = {
286
+ x: cx + rand(-16, 16),
287
+ h: rand(H * 0.12, H * 0.34),
288
+ segs: 8,
289
+ phase: rand(0, Math.PI * 2),
290
+ speed: rand(0.9, 1.7),
291
+ hue: clusterHue + rand(-10, 10),
292
+ width: rand(4, 8),
293
+ touch: 0,
294
+ off: new Float32Array(9), // lateral bend per segment (fish push leaves aside)
295
+ offV: new Float32Array(9), // velocity of that bend
296
+ branches: [],
297
+ points: []
298
+ };
299
+ const nBr = 1 + ((Math.random() * 3) | 0); // 1..3 branches per stalk
300
+ for (let k = 0; k < nBr; k++) {
301
+ st.branches.push({
302
+ f: rand(0.25, 0.85), // where along the stalk it sprouts
303
+ side: Math.random() < 0.5 ? -1 : 1,
304
+ len: rand(12, 30), // horizontal reach
305
+ up: rand(10, 26), // how far it curls upward
306
+ w: rand(2, 4),
307
+ ph: rand(0, Math.PI * 2),
308
+ flower: Math.random() < 0.3 ? { // ~30% of branch tips bloom
309
+ hue: (Math.random() * 360) | 0, // colourful — full spectrum
310
+ r: rand(4.5, 8), // head size
311
+ petals: 5 + ((Math.random() * 3) | 0) // 5..7 petals
312
+ } : null
313
+ });
314
+ }
315
+ seaweeds.push(st);
316
+ }
317
+ }
318
+
319
+ // Short, dense grass-like ground cover along the sand bed.
320
+ // Each tuft is a cluster of thin blades that ripple when fish swim nearby.
321
+ const nGrassTufts = Math.max(50, Math.floor(W / 9));
322
+ for (let i = 0; i < nGrassTufts; i++) {
323
+ const gx = rand(8, W - 8);
324
+ const gy = sandAt(gx) + rand(-3, 7);
325
+ const baseHue = rand(95, 155);
326
+ const blades = 2 + ((Math.random() * 4) | 0); // 2..5 blades per tuft
327
+ for (let b = 0; b < blades; b++) {
328
+ grass.push({
329
+ x: gx + rand(-7, 7),
330
+ y: gy,
331
+ h: rand(14, 44),
332
+ lean: rand(-0.35, 0.35), // base tilt from vertical
333
+ phase: rand(0, Math.PI * 2),
334
+ speed: rand(1.3, 2.8), // quicker, finer sway than seaweed
335
+ hue: baseHue + rand(-9, 9),
336
+ width: rand(1.4, 2.8),
337
+ ripple: 0 // agitation from nearby fish (0..1)
338
+ });
339
+ }
340
+ }
341
+
342
+ const nFish = Math.max(7, Math.floor(W / 130));
343
+ for (let i = 0; i < nFish; i++) fishes.push(new Fish());
344
+
345
+ for (let i = 0; i < 52; i++) bubbles.push(new Bubble(true)); // doubled for a livelier stream
346
+
347
+ for (let i = 0; i < 45; i++) {
348
+ motes.push({ x: rand(0, W), y: rand(0, H), r: rand(0.6, 1.8), s: rand(3, 9), p: rand(0, Math.PI * 2) });
349
+ }
350
+
351
+ for (let i = 0; i < 5; i++) {
352
+ rays.push({
353
+ x: rand(0, W), w: rand(80, 210), tilt: rand(-0.35, 0.35),
354
+ speed: rand(0.06, 0.14) * (Math.random() < 0.5 ? 1 : -1), alpha: rand(0.04, 0.09)
355
+ });
356
+ }
357
+
358
+ const nRocks = Math.max(3, Math.floor(W / 320));
359
+ for (let i = 0; i < nRocks; i++) {
360
+ rocks.push({ x: rand(40, W - 40), r: rand(14, 34), tone: rand(0.75, 1) });
361
+ }
362
+
363
+ starfish = { x: rand(W * 0.2, W * 0.8), r: rand(12, 18), rot: rand(0, Math.PI * 2) };
364
+ }
365
+
366
+ function resize() {
367
+ const dpr = Math.min(2, window.devicePixelRatio || 1);
368
+ W = window.innerWidth;
369
+ H = window.innerHeight;
370
+ canvas.width = W * dpr;
371
+ canvas.height = H * dpr;
372
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
373
+ buildScene();
374
+ }
375
+
376
+ // --- slow light cycle: dark -> blue -> orange -> dark over CYCLE seconds ---
377
+ const CYCLE = 240; // seconds per full day/night cycle (night now lasts ~2x as long)
378
+ // [t, waterTop, waterMid, waterBot, rayTint, ambientWashRGB, ambientWashAlpha]
379
+ const LIGHT_KEYS = [
380
+ [0.00, [13,88,118], [8,58,82], [3,25,43], [190,235,255], [190,235,255], 0.00],
381
+ [0.35, [224,122,52], [128,60,36], [34,22,26], [255,176,96], [255,150,70], 0.14],
382
+ [0.62, [7,15,32], [4,10,22], [2,6,14], [96,146,206], [3,8,20], 0.40],
383
+ [0.90, [7,15,32], [4,10,22], [2,6,14], [96,146,206], [3,8,20], 0.40],
384
+ [1.00, [13,88,118], [8,58,82], [3,25,43], [190,235,255], [190,235,255], 0.00]
385
+ ];
386
+
387
+ function sampleLight(t) {
388
+ const p = ((t / CYCLE) % 1 + 1) % 1;
389
+ let i = 0;
390
+ while (i < LIGHT_KEYS.length - 2 && LIGHT_KEYS[i + 1][0] <= p) i++;
391
+ const k0 = LIGHT_KEYS[i], k1 = LIGHT_KEYS[i + 1];
392
+ const f = Math.max(0, Math.min(1, (p - k0[0]) / (k1[0] - k0[0])));
393
+ // each key row: [t, waterTop[r,g,b], waterMid, waterBot, rayTint, washRGB, washA]
394
+ const mixC = (a, b) => [0, 1, 2].map(c => Math.round(a[c] + (b[c] - a[c]) * f));
395
+ return { top: mixC(k0[1], k1[1]), mid: mixC(k0[2], k1[2]), bot: mixC(k0[3], k1[3]), ray: mixC(k0[4], k1[4]), washRGB: mixC(k0[5], k1[5]), washA: k0[6] + (k1[6] - k0[6]) * f };
396
+ }
397
+
398
+ function rgbStr(c) { return `rgb(${c[0]},${c[1]},${c[2]})`; }
399
+
400
+ function drawBackground(light) {
401
+ const g = ctx.createLinearGradient(0, 0, 0, H);
402
+ g.addColorStop(0, rgbStr(light.top));
403
+ g.addColorStop(0.5, rgbStr(light.mid));
404
+ g.addColorStop(1, rgbStr(light.bot));
405
+ ctx.fillStyle = g;
406
+ ctx.fillRect(0, 0, W, H);
407
+ }
408
+
409
+ function drawRays(t, light) {
410
+ const [rr, rg, rb] = light.ray;
411
+ for (const r of rays) {
412
+ const off = Math.sin(t * r.speed * 6 + r.x) * 55;
413
+ const x = r.x + off;
414
+ const topW = r.w * 0.45, botW = r.w * 1.9;
415
+ const shift = r.tilt * H;
416
+
417
+ // soft outer beam: bright at the surface, fading down through the water
418
+ let grad = ctx.createLinearGradient(0, 0, 0, H);
419
+ grad.addColorStop(0, `rgba(${rr},${rg},${rb},${r.alpha})`);
420
+ grad.addColorStop(0.55, `rgba(${rr},${rg},${rb},${r.alpha * 0.45})`);
421
+ grad.addColorStop(1, `rgba(${rr},${rg},${rb},0)`);
422
+ ctx.fillStyle = grad;
423
+ ctx.beginPath();
424
+ ctx.moveTo(x - topW, -10);
425
+ ctx.lineTo(x + topW, -10);
426
+ ctx.lineTo(x + shift + botW, H * 0.95);
427
+ ctx.lineTo(x + shift - botW, H * 0.95);
428
+ ctx.closePath();
429
+ ctx.fill();
430
+
431
+ // brighter inner core so each beam has a gentle gradient across it
432
+ const cTop = topW * 0.42, cBot = botW * 0.42;
433
+ grad = ctx.createLinearGradient(0, 0, 0, H);
434
+ grad.addColorStop(0, `rgba(${rr},${rg},${rb},${r.alpha * 1.5})`);
435
+ grad.addColorStop(0.65, `rgba(${rr},${rg},${rb},0)`);
436
+ ctx.fillStyle = grad;
437
+ ctx.beginPath();
438
+ ctx.moveTo(x - cTop, -10);
439
+ ctx.lineTo(x + cTop, -10);
440
+ ctx.lineTo(x + shift + cBot, H * 0.95);
441
+ ctx.lineTo(x + shift - cBot, H * 0.95);
442
+ ctx.closePath();
443
+ ctx.fill();
444
+ }
445
+ }
446
+
447
+ function updateMotes(dt, t) {
448
+ ctx.fillStyle = "rgba(220,240,255,0.3)";
449
+ for (const m of motes) {
450
+ m.y -= m.s * dt;
451
+ m.x += Math.sin(t + m.p) * 6 * dt;
452
+ if (m.y < -4) { m.y = H + 4; m.x = rand(0, W); }
453
+ ctx.beginPath();
454
+ ctx.arc(m.x, m.y, m.r, 0, Math.PI * 2);
455
+ ctx.fill();
456
+ }
457
+ }
458
+
459
+ function drawSand() {
460
+ const g = ctx.createLinearGradient(0, H * 0.85, 0, H);
461
+ g.addColorStop(0, "#d9b97c");
462
+ g.addColorStop(1, "#7e653d");
463
+ ctx.fillStyle = g;
464
+ ctx.beginPath();
465
+ ctx.moveTo(-10, H + 10);
466
+ for (let i = 0; i < sandHeights.length; i++) ctx.lineTo(i * 40, sandHeights[i]);
467
+ ctx.lineTo(W + 10, H + 10);
468
+ ctx.closePath();
469
+ ctx.fill();
470
+ }
471
+
472
+ function drawRocks() {
473
+ for (const rk of rocks) {
474
+ const y = sandAt(rk.x) + rk.r * 0.35;
475
+ // fully opaque — tone now varies lightness instead of alpha
476
+ const base = Math.round(28 + rk.tone * 46);
477
+ const g = ctx.createRadialGradient(rk.x - rk.r * 0.3, y - rk.r * 0.4, rk.r * 0.1, rk.x, y, rk.r);
478
+ g.addColorStop(0, `rgb(${base + 68},${base + 78},${base + 92})`);
479
+ g.addColorStop(1, "rgb(38,48,62)");
480
+ ctx.fillStyle = g;
481
+ ctx.beginPath();
482
+ ctx.ellipse(rk.x, y, rk.r, rk.r * 0.72, 0, Math.PI, Math.PI * 2);
483
+ ctx.fill();
484
+ }
485
+ }
486
+
487
+ function drawStarfish(t) {
488
+ const sf = starfish;
489
+ const y = sandAt(sf.x) - sf.r * 0.15;
490
+ const rot = sf.rot + Math.sin(t * 0.4 + sf.x) * 0.08;
491
+ ctx.save();
492
+ ctx.translate(sf.x, y);
493
+ ctx.rotate(rot);
494
+ ctx.fillStyle = "#ff7a59";
495
+ ctx.beginPath();
496
+ for (let i = 0; i < 10; i++) {
497
+ const ang = i * Math.PI / 5 - Math.PI / 2;
498
+ const rr = i % 2 === 0 ? sf.r : sf.r * 0.45;
499
+ const px = Math.cos(ang) * rr, py = Math.sin(ang) * rr;
500
+ i ? ctx.lineTo(px, py) : ctx.moveTo(px, py);
501
+ }
502
+ ctx.closePath();
503
+ ctx.fill();
504
+ ctx.fillStyle = "rgba(255,255,255,0.35)";
505
+ for (let i = 0; i < 5; i++) {
506
+ const ang = i * Math.PI * 2 / 5 - Math.PI / 2;
507
+ ctx.beginPath();
508
+ ctx.arc(Math.cos(ang) * sf.r * 0.5, Math.sin(ang) * sf.r * 0.5, 1.4, 0, Math.PI * 2);
509
+ ctx.fill();
510
+ }
511
+ ctx.restore();
512
+ }
513
+
514
+ function updatePlants(t, dt) {
515
+ plantPoints.length = 0;
516
+ for (const w of seaweeds) {
517
+ // ---- fish push leaves aside: fish overpower the flora ----
518
+ for (const f of fishes) {
519
+ const R = 80, R2 = R * R; // influence radius around fish
520
+ const fx = Math.cos(f.angle) * f.speed * 60; // fish velocity x (px/s)
521
+ for (let i = 1; i <= w.segs; i++) {
522
+ const fl = i / w.segs;
523
+ const px = w.x + w.off[i] + Math.sin(t * w.speed + w.phase + fl * 2.6) * fl * fl * 24;
524
+ const py = sandAt(w.x) + 8 - w.h * fl;
525
+ const dx = px - f.x, dy = py - f.y;
526
+ const d2 = dx * dx + dy * dy;
527
+ if (d2 < R2 && d2 > 0.01) {
528
+ const d = Math.sqrt(d2);
529
+ const weight = (R - d) / R * (0.35 + 0.65 * fl); // upper segments yield more
530
+ // shove radially away from the fish + drag along its swim direction
531
+ w.offV[i] += ((dx / d) * 1500 + fx * 9) * weight * dt;
532
+ if (weight > 0.25) w.touch = Math.min(1, w.touch + dt * 5);
533
+ }
534
+ }
535
+ }
536
+
537
+ // ---- spring back toward rest, with damping (leaves flow back after fish pass) ----
538
+ for (let i = 1; i <= w.segs; i++) {
539
+ w.offV[i] += -w.off[i] * 20 * dt;
540
+ w.offV[i] *= Math.max(0, 1 - 4 * dt);
541
+ }
542
+
543
+ // ---- neighbour coupling: bend as one flexible stalk, not loose points ----
544
+ for (let i = 1; i < w.segs; i++) {
545
+ const F = (w.off[i + 1] - w.off[i]) * 30 * dt;
546
+ w.offV[i] += F;
547
+ w.offV[i + 1] -= F;
548
+ }
549
+
550
+ // ---- integrate bend (root stays planted) ----
551
+ for (let i = 1; i <= w.segs; i++) {
552
+ w.off[i] += w.offV[i] * dt;
553
+ if (w.off[i] > 70) { w.off[i] = 70; w.offV[i] *= -0.3; }
554
+ else if (w.off[i] < -70) { w.off[i] = -70; w.offV[i] *= -0.3; }
555
+ }
556
+
557
+ if (w.touch > 0) w.touch = Math.max(0, w.touch - dt * 1.2);
558
+ const baseY = sandAt(w.x) + 8;
559
+ w.points.length = 0;
560
+ for (let i = 0; i <= w.segs; i++) {
561
+ const f = i / w.segs;
562
+ // normal sway + a fast extra flinch while being pushed
563
+ const sway = Math.sin(t * w.speed + w.phase + f * 2.6) * f * f * 24
564
+ + Math.sin(t * 7 + w.phase) * w.touch * f * 16;
565
+ const px = w.x + (w.off[i] || 0) + sway;
566
+ const py = baseY - w.h * f;
567
+ w.points.push({ x: px, y: py });
568
+ plantPoints.push({ x: px, y: py, r: w.width * 0.55, s: w });
569
+ }
570
+ for (const b of w.branches) {
571
+ const a = w.points[Math.min(w.segs, Math.max(1, Math.round(b.f * w.segs)))];
572
+ const flutter = Math.sin(t * w.speed * 1.4 + b.ph) * (3 + w.touch * 9);
573
+ b.tx = a.x + b.side * b.len + flutter;
574
+ b.ty = a.y - b.up;
575
+ plantPoints.push({ x: b.tx, y: b.ty, r: 5, s: w });
576
+ plantPoints.push({ x: (a.x + b.tx) / 2, y: (a.y + b.ty) / 2, r: 4, s: w });
577
+ }
578
+ }
579
+ }
580
+
581
+ // Grass blades ripple when fish swim close — stronger the lower the fish is.
582
+ function updateGrass(t, dt) {
583
+ for (const g of grass) {
584
+ if (g.ripple > 0) g.ripple = Math.max(0, g.ripple - dt * 1.6);
585
+ for (const f of fishes) {
586
+ const dx = g.x - f.x;
587
+ const dy = g.y - f.y;
588
+ const d2 = dx * dx + dy * dy;
589
+ const R = 95, R2 = R * R;
590
+ if (d2 < R2 && d2 > 1) {
591
+ const d = Math.sqrt(d2);
592
+ // fish low near the ground stir the grass far more than high up
593
+ const nearGround = f.y > H * 0.58 ? 1 : 0.35;
594
+ g.ripple = Math.min(1, g.ripple + ((R - d) / R) * dt * 9 * nearGround);
595
+ }
596
+ }
597
+ }
598
+ }
599
+
600
+ function drawSeaweed() {
601
+ for (const w of seaweeds) {
602
+ // main stalk through its precomputed (collision-accurate) points
603
+ ctx.strokeStyle = `hsla(${w.hue},55%,36%,0.92)`;
604
+ ctx.lineWidth = w.width;
605
+ ctx.lineCap = "round";
606
+ ctx.beginPath();
607
+ ctx.moveTo(w.points[0].x, w.points[0].y);
608
+ for (let i = 1; i < w.points.length; i++) ctx.lineTo(w.points[i].x, w.points[i].y);
609
+ ctx.stroke();
610
+
611
+ // side branches curling up and out from the stalk
612
+ for (const b of w.branches) {
613
+ const a = w.points[Math.min(w.segs, Math.max(1, Math.round(b.f * w.segs)))];
614
+ ctx.strokeStyle = `hsla(${w.hue + 8},60%,46%,0.9)`;
615
+ ctx.lineWidth = b.w;
616
+ ctx.beginPath();
617
+ ctx.moveTo(a.x, a.y);
618
+ ctx.quadraticCurveTo((a.x + b.tx) / 2 + b.side * 3, (a.y + b.ty) / 2 - 5, b.tx, b.ty);
619
+ ctx.stroke();
620
+
621
+ // colourful flower head blooming at the branch tip
622
+ if (b.flower) drawFlowerHead(b.tx, b.ty, b.flower);
623
+ }
624
+ }
625
+ }
626
+
627
+ function drawFlowerHead(x, y, fl) {
628
+ ctx.save();
629
+ for (let i = 0; i < fl.petals; i++) { // petals radiating from the centre
630
+ const ang = (i / fl.petals) * Math.PI * 2;
631
+ const px = x + Math.cos(ang) * fl.r * 0.72;
632
+ const py = y + Math.sin(ang) * fl.r * 0.72;
633
+ ctx.fillStyle = `hsla(${fl.hue},85%,64%,0.95)`;
634
+ ctx.beginPath();
635
+ ctx.ellipse(px, py, fl.r * 0.55, fl.r * 0.36, ang, 0, Math.PI * 2);
636
+ ctx.fill();
637
+ }
638
+ ctx.fillStyle = `hsla(${(fl.hue + 45) % 360},92%,78%,0.95)`; // bright contrasting centre
639
+ ctx.beginPath();
640
+ ctx.arc(x, y, fl.r * 0.42, 0, Math.PI * 2);
641
+ ctx.fill();
642
+ ctx.restore();
643
+ }
644
+
645
+ function drawGrass(t) {
646
+ for (const g of grass) {
647
+ // gentle constant sway, amplified while rippling from a passing fish
648
+ const amp = 3 + g.ripple * 13;
649
+ const sway = Math.sin(t * g.speed + g.phase) * amp;
650
+ const tipX = g.x + g.lean * g.h + sway;
651
+ const tipY = g.y - g.h * (1 - g.ripple * 0.06);
652
+ ctx.strokeStyle = `hsla(${g.hue},58%,40%,0.92)`;
653
+ ctx.lineWidth = g.width;
654
+ ctx.lineCap = "round";
655
+ ctx.beginPath();
656
+ ctx.moveTo(g.x, g.y);
657
+ ctx.quadraticCurveTo(
658
+ (g.x + tipX) / 2 + sway * 0.4,
659
+ (g.y + tipY) / 2,
660
+ tipX, tipY
661
+ );
662
+ ctx.stroke();
663
+ }
664
+ }
665
+
666
+ function drawVignette() {
667
+ const g = ctx.createRadialGradient(W / 2, H * 0.45, Math.min(W, H) * 0.35, W / 2, H * 0.55, Math.max(W, H) * 0.78);
668
+ g.addColorStop(0, "rgba(0,0,0,0)");
669
+ g.addColorStop(1, "rgba(0,10,20,0.45)");
670
+ ctx.fillStyle = g;
671
+ ctx.fillRect(0, 0, W, H);
672
+ }
673
+
674
+ let last = performance.now();
675
+ function frame(now) {
676
+ const dt = Math.min(0.05, (now - last) / 1000);
677
+ last = now;
678
+ const t = now / 1000;
679
+
680
+ const light = sampleLight(t);
681
+ drawBackground(light);
682
+ drawRays(t, light);
683
+ updateMotes(dt, t);
684
+
685
+ updatePlants(t, dt);
686
+ updateGrass(t, dt);
687
+
688
+ for (const f of fishes) f.update(dt);
689
+ fishes.sort((a, b) => a.size - b.size); // small fish swim behind
690
+ for (const f of fishes) f.draw(ctx);
691
+
692
+ drawSand();
693
+ drawRocks();
694
+ drawStarfish(t);
695
+ drawSeaweed();
696
+ drawGrass(t);
697
+
698
+ for (let i = bubbles.length - 1; i >= 0; i--) {
699
+ const b = bubbles[i];
700
+ b.update(dt, t);
701
+ if (b.dead) { bubbles.splice(i, 1); continue; }
702
+ b.draw(ctx);
703
+ }
704
+
705
+ // ambient day/night wash over the whole scene
706
+ if (light.washA > 0.005) {
707
+ ctx.fillStyle = `rgba(${light.washRGB[0]},${light.washRGB[1]},${light.washRGB[2]},${light.washA})`;
708
+ ctx.fillRect(0, 0, W, H);
709
+ }
710
+
711
+ drawVignette();
712
+ requestAnimationFrame(frame);
713
+ }
714
+
715
+ window.addEventListener("resize", resize);
716
+ resize();
717
+ requestAnimationFrame(frame);
718
+ })();
719
+ </script>
720
+ </body>
721
+ </html>