One commited on
Commit
8cbb622
·
verified ·
1 Parent(s): 27e7c18

Upload index.html

Browse files
Files changed (1) hide show
  1. index.html +97 -91
index.html CHANGED
@@ -88,48 +88,12 @@ function genKoiPatches(type) {
88
  }
89
  let fishes = [], bubbles = [], seaweeds = [], backSeaweeds = [], midSeaweeds = [], farSeaweeds = [], motes = [], rays = [], rocks = [], grass = [], fgGrass = [];
90
  let playerFish = null; // the fish controlled by gamepad (spawned with F key)
91
- let bubblePool = [];
92
- let bubblePoolSize = 0;
93
-
94
- function getBubble() {
95
- if (bubblePool.length > 0) {
96
- return bubblePool.pop();
97
- }
98
- bubblePoolSize++;
99
- return new Bubble(true);
100
- }
101
- function returnBubble(b) {
102
- if (bubblePool.length < 50) {
103
- bubblePool.push(b);
104
- }
105
- }
106
  const GAMEPAD_DEADZONE = 0.15; // ignore small stick movements
107
  let curNight = 0; // v7: shared night factor (dims the top-light + sheen)
108
  let plantPoints = [], _ppUsed = 0; // collision points — a pooled set, reused in place
109
  let sandHeights = [];
110
  let sandParticles = []; // v22: grains of sand kicked up when a fast fish skims the sea floor
111
 
112
-
113
- // v26: Trig lookup table for faster sin/cos (360 entries, 1 degree resolution)
114
- const SIN_TABLE = new Float32Array(360);
115
- const COS_TABLE = new Float32Array(360);
116
- for (let i = 0; i < 360; i++) {
117
- const rad = i * Math.PI / 180;
118
- SIN_TABLE[i] = Math.sin(rad);
119
- COS_TABLE[i] = Math.cos(rad);
120
- }
121
- function fastSin(x) {
122
- // Convert radians to degrees, normalize to 0-360
123
- let deg = (x * 180 / Math.PI) % 360;
124
- if (deg < 0) deg += 360;
125
- return SIN_TABLE[Math.floor(deg)];
126
- }
127
- function fastCos(x) {
128
- let deg = (x * 180 / Math.PI) % 360;
129
- if (deg < 0) deg += 360;
130
- return COS_TABLE[Math.floor(deg)];
131
- }
132
-
133
  class Fish {
134
  static nextId = 0;
135
  constructor() {
@@ -402,11 +366,11 @@ class Fish {
402
  // Occasionally release a small cluster of bubbles from the mouth
403
  this.bubbleTimer -= dt;
404
  if (this.bubbleTimer <= 0) {
405
- const mx = this.x + fastCos(this.angle) * this.size * 0.62;
406
  const my = this.y + Math.sin(this.angle) * this.size * 0.62;
407
  const n = (2 + ((Math.random() * 3) | 0)) * 2; // 4..8 bubbles — doubled from the old 2..4
408
  for (let i = 0; i < n; i++) {
409
- const bb = getBubble(); bb.reset(false); bb.x = mx + rand(-3, 3); bb.y = my + rand(-3, 3); bb.r = rand(1.4, 3.2) * (this.size / 28); bb.speed = rand(45, 85) * (0.7 + 0.3 * (this.size / 28)); bb.p = rand(0, Math.PI * 2); bb.rgb = bubbleColor(); bb.isMouth = true;
410
  bb.owner = this; // v10: a fish never pops its own bubbles
411
  bubbles.push(bb);
412
  }
@@ -418,7 +382,7 @@ class Fish {
418
 
419
  // Target sits ahead of the fish (in heading direction), offset by wanderTheta.
420
  // Steering toward it yields smooth meandering and genuine direction changes.
421
- let wx = this.x + fastCos(this.angle) * this.wanderDist + Math.cos(this.wanderTheta) * this.wanderRadius;
422
  let wy = this.y + Math.sin(this.angle) * this.wanderDist + Math.sin(this.wanderTheta) * this.wanderRadius;
423
 
424
  // v7: after dark, most fish bed down low in the grass; a few still hover by the seaweed.
@@ -427,7 +391,7 @@ class Fish {
427
  // v14: drift toward this fish's own sleep spot along the sand line —
428
  // spreads sleeping fish across the tank instead of one big clump
429
  wx += (this.sleepSpotX - wx) * night * 0.75;
430
- const groundY = sandAt(this.x + fastCos(this.angle) * this.wanderDist) - this.size * 0.5;
431
  wy += (groundY - wy) * night * 0.85;
432
  } else if (seaweeds.length) {
433
  // v9: each seaweed-hungry fish keeps to its own stalk instead of all crowding the nearest one
@@ -564,13 +528,13 @@ class Fish {
564
  if (inp.slow > 0) speedMult -= inp.slow * 0.85; // slow down
565
  speedMult = Math.max(0.15, Math.min(3.0, speedMult));
566
  const playerSpeed = this.speed * this.cruiseSpeed * speedMult * 60 * dt;
567
- this.x += fastCos(this.angle) * playerSpeed;
568
  this.y += Math.sin(this.angle) * playerSpeed;
569
  this.currentVelocity = this.speed * this.cruiseSpeed * speedMult * 60; // track actual velocity
570
  } else {
571
  // Normal AI fish movement.
572
  const sp = this.speed * this.cruiseSpeed * this.speedFactor * 60 * dt;
573
- this.x += fastCos(this.angle) * sp;
574
  this.y += Math.sin(this.angle) * sp;
575
  this.currentVelocity = this.speed * this.cruiseSpeed * this.speedFactor * 60; // track actual velocity
576
  }
@@ -601,7 +565,7 @@ class Fish {
601
  const v = Math.max(0, Math.min(1, this.viewAngle));
602
  let scaleY = 1 - 0.34 * v; // foreshorten toward the back view
603
  if (Math.abs(this.roll) > 0.01) { // banking roll still squashes a touch
604
- scaleY *= Math.max(0.42, fastCos(this.roll));
605
  c.rotate(this.roll * 0.1);
606
  }
607
  c.scale(1, scaleY);
@@ -616,7 +580,7 @@ class Fish {
616
  // traveling body wave
617
  const waveK = Math.PI / s;
618
  const waveAmp = s * 0.12;
619
- const sway = fastSin(-s * waveK + this.phase) * s * 0.34;
620
 
621
  // Top/bottom view: body seen from above or below
622
  // Dorsal fin visible as a line down the center when viewed from above
@@ -701,7 +665,7 @@ class Fish {
701
  const waveK = Math.PI / s; // ~one wavelength across the body length
702
  const waveAmp = s * 0.12; // subtle flex amplitude
703
  // tail fin follows the body wave at its base, amplified (same phase → no fighting)
704
- const sway = fastSin(-s * waveK + this.phase) * s * 0.34;
705
 
706
  // v24: Ecco-style blend factor (0 = side profile, 1 = seen from above/below).
707
  // Flattens the tail lobes and spreads eyes/barbels as we see more of the back.
@@ -762,7 +726,7 @@ class Fish {
762
  const bx = rootX - L * (0.25 + 0.65 * spd) * t;
763
  const by = rootY + side * L * (0.42 + 0.3 * ev) * t * t;
764
  const amp = s * 0.17 * t * (0.35 + 0.65 * spd);
765
- const off = fastSin(t * 5.5 - this.phase * 1.4 + this.whiskPh) * amp;
766
  c.lineTo(bx, by + off);
767
  }
768
  c.stroke();
@@ -828,16 +792,16 @@ class Fish {
828
  // left edge, top → bottom: bend the centerline + taper the width with t
829
  for (let j = 0; j <= N; j++) {
830
  const t = j / N;
831
- const cx = sx + fastSin(t * st.cFreq * Math.PI + st.cPh) * st.cAmp * s;
832
- const hwL = hw0 * (1 + st.tAmp * fastSin(t * st.tFreq * Math.PI + st.tPh));
833
  const xL = cx - hwL, yL = wy(xL) + (t - 0.5) * H;
834
  if (j === 0) c.moveTo(xL, yL); else c.lineTo(xL, yL);
835
  }
836
  // right edge, bottom → top
837
  for (let j = N; j >= 0; j--) {
838
  const t = j / N;
839
- const cx = sx + fastSin(t * st.cFreq * Math.PI + st.cPh) * st.cAmp * s;
840
- const hwR = hw0 * (1 + st.tAmp * fastSin(t * st.tFreq * Math.PI + st.tPh));
841
  const xR = cx + hwR, yR = wy(xR) + (t - 0.5) * H;
842
  c.lineTo(xR, yR);
843
  }
@@ -984,7 +948,7 @@ class Fish {
984
  const bx = rootX - L * (0.25 + 0.65 * spd) * t;
985
  const by = rootY + side * L * (0.42 + 0.3 * ev) * t * t;
986
  const amp = s * 0.17 * t * (0.35 + 0.65 * spd);
987
- const off = fastSin(t * 5.5 - this.phase * 1.4 + this.whiskPh) * amp;
988
  c.lineTo(bx, by + off);
989
  }
990
  c.stroke();
@@ -1047,7 +1011,6 @@ class Bubble {
1047
  }
1048
  }
1049
  reset(init) {
1050
- this.dead = false;
1051
  this.x = rand(0, W);
1052
  this.y = init ? rand(H * 0.3, H) : H + rand(10, 80);
1053
  this.r = rand(2, 7);
@@ -1058,7 +1021,7 @@ class Bubble {
1058
  }
1059
  update(dt, t) {
1060
  this.y -= this.speed * dt;
1061
- this.x += fastSin(t * 1.6 + this.p) * 22 * dt;
1062
  if (this.y < -12) { if (this.isMouth) this.dead = true; else this.reset(false); }
1063
  }
1064
  draw(c) {
@@ -1373,9 +1336,8 @@ function buildScene() {
1373
  sPh: rand(0, Math.PI * 2),
1374
  // PERF: per-blade scratch points — drawGrass reuses them instead of
1375
  // allocating a fresh pts array + point objects every frame (GC churn)
1376
- pts: Array.from({ length: bladeSegs + 1 }, () => ({ x: 0, y: 0 })),
1377
- fillStyle: `hsla(${baseHue + rand(-9, 9)},58%,40%,0.92)`,
1378
- strokeStyle: `hsla(${baseHue + rand(-9, 9) + 6},64%,45%,0.5)` });
1379
  }
1380
  }
1381
 
@@ -1415,9 +1377,8 @@ function buildScene() {
1415
  // v28: ~40% of blades get a gentle blur (pre-rendered sprite) for depth
1416
  blurred: Math.random() < 0.4,
1417
  sprite: null,
1418
- spriteScale: 1,
1419
- fillStyle: `hsla(${baseHue + rand(-10, 10)},60%,31%,0.96)`,
1420
- strokeStyle: `hsla(${baseHue + rand(-10, 10) + 6},64%,45%,0.5)` });
1421
  }
1422
  }
1423
  buildFgGrassSprites(); // v28: bake the defocused-blade sprites once (free at runtime)
@@ -1432,7 +1393,7 @@ function buildScene() {
1432
  for (let i = 0; i < nFish; i++) fishes.push(new Fish());
1433
 
1434
  const nAmbient = Math.round(26 * 0.75); // v10: ambient bubbles reduced 25% (fish production unchanged)
1435
- for (let i = 0; i < nAmbient; i++) { const b = getBubble(); b.reset(true); bubbles.push(b); }
1436
 
1437
  for (let i = 0; i < 45; i++) {
1438
  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) });
@@ -1690,10 +1651,13 @@ function drawRays(t, light) {
1690
  applyRayTint(); // cheap re-tint of the cached blurred sprites (no blur re-run)
1691
  }
1692
 
 
 
 
1693
  for (const r of rays) {
1694
  // blit horizontal slices with the undulation offset at each depth — same wave
1695
  // math as before, so the beams shimmer exactly like they always have
1696
- const off = fastSin(t * r.speed * 6 + r.x) * 55; // slow drift of the whole beam
1697
  for (let i = 0; i < RAY_SLICES; i++) {
1698
  const vT = i / RAY_SLICES, vB = (i + 1) / RAY_SLICES, vM = (vT + vB) / 2;
1699
  const ya = yTop0 + vT * ySpan, yb = yTop0 + vB * ySpan;
@@ -1709,6 +1673,47 @@ function drawRays(t, light) {
1709
  }
1710
  }
1711
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1712
  function updateMotes(dt, t) {
1713
  ctx.fillStyle = "rgba(220,240,255,0.3)";
1714
  for (const m of motes) {
@@ -1761,9 +1766,9 @@ function stalkCurveX(w, f) {
1761
 
1762
  // PERF: shared sway function (was a per-stalk closure recreated every frame)
1763
  function stalkSwayX(w, t, f) {
1764
- return fastSin(t * w.waveF1 + w.wavePh1 + f * 2.4) * w.waveA1 * f * f +
1765
- fastSin(t * w.waveF2 + w.wavePh2 + f * 3.8) * w.waveA2 * f * f +
1766
- fastSin(t * w.waveF3 + w.wavePh3 + f * 5.4) * w.waveA3 * f;
1767
  }
1768
 
1769
  // v4.3: draw background seaweed procedurally — multi-wave organic motion instead of
@@ -1823,7 +1828,7 @@ function drawMidSeaweeds(t) {
1823
  for (const b of w.branches) {
1824
  const idx = Math.min(n, Math.max(1, Math.round(b.f * n)));
1825
  const ax = pxs[idx], ay = pys[idx];
1826
- const flutter = fastSin(t * w.waveF2 * 1.4 + b.ph) * (3 + w.waveA2 * 0.4);
1827
  const bx = ax + b.side * b.len + flutter;
1828
  const by = ay - b.up;
1829
  ctx.beginPath();
@@ -1895,7 +1900,7 @@ function drawBackSeaweeds(t) {
1895
  for (const b of w.branches) {
1896
  const idx = Math.min(n, Math.max(1, Math.round(b.f * n)));
1897
  const ax = pxs[idx], ay = pys[idx];
1898
- const flutter = fastSin(t * w.waveF2 * 1.4 + b.ph) * (3 + w.waveA2 * 0.4);
1899
  const bx = ax + b.side * b.len + flutter;
1900
  const by = ay - b.up;
1901
  ctx.beginPath();
@@ -2086,7 +2091,7 @@ function drawFarSeaweeds(t) {
2086
 
2087
  const baseY = sandAt(w.x) + 10;
2088
  // Simple rotational sway - root stays planted, sprite rotates slightly
2089
- const swayAngle = fastSin(t * w.speed + w.phase) * 0.04; // very subtle rotation
2090
 
2091
  ctx.save();
2092
  // Translate to the base point (where the stalk meets the sand)
@@ -2112,7 +2117,7 @@ function updatePlants(t, dt) {
2112
  const moveF = Math.min(1, fSpd / 1.2); // v9: how actively this fish is swimming
2113
  for (let i = 1; i <= w.segs; i++) {
2114
  const fl = i / w.segs;
2115
- const px = w.x + w.off[i] + stalkCurveX(w, fl) + fastSin(t * w.speed + w.phase + fl * 2.6) * fl * fl * 24;
2116
  const py = sandAt(w.x) + 8 - w.h * fl;
2117
  const dx = px - f.x, dy = py - f.y;
2118
  const d2 = dx * dx + dy * dy;
@@ -2154,10 +2159,10 @@ function updatePlants(t, dt) {
2154
  for (let i = 0; i <= w.segs; i++) {
2155
  const f = i / w.segs;
2156
  // normal sway + a fast extra flinch while being pushed
2157
- const sway = fastSin(t * w.speed + w.phase + f * 2.6) * f * f * 24
2158
- + fastSin(t * 7 + w.phase) * w.touch * f * 16;
2159
  // v17: secondary wobble - a finer inner movement for unique per-stalk motion
2160
- const sWobble = fastSin(t * w.sFreq + w.sPh + f * 4.2) * w.sAmp * f;
2161
  const px = w.x + (w.off[i] || 0) + stalkCurveX(w, f) + sway + sWobble;
2162
  const py = baseY - w.h * f;
2163
  w.points.push({ x: px, y: py });
@@ -2165,7 +2170,7 @@ function updatePlants(t, dt) {
2165
  }
2166
  for (const b of w.branches) {
2167
  const a = w.points[Math.min(w.segs, Math.max(1, Math.round(b.f * w.segs)))];
2168
- const flutter = fastSin(t * w.speed * 1.4 + b.ph) * (3 + w.touch * 9);
2169
  b.tx = a.x + b.side * b.len + flutter;
2170
  b.ty = a.y - b.up;
2171
  plantPoints.push({ x: b.tx, y: b.ty, r: 5, s: w });
@@ -2209,7 +2214,7 @@ function drawSeaweed() {
2209
  const p = pts[i];
2210
  const a = pts[Math.max(0, i - 1)], b = pts[Math.min(n - 1, i + 1)];
2211
  let dx = b.x - a.x, dy = b.y - a.y;
2212
- const dl = Math.sqrt(dx*dx + dy*dy) || 1;
2213
  const nx = -dy / dl, ny = dx / dl; // perpendicular to the local stalk direction
2214
  const f = i / (n - 1);
2215
  const hw = (w.width * (1 - f) + w.tipW * f) / 2; // half-width: base → tip taper
@@ -2219,7 +2224,7 @@ function drawSeaweed() {
2219
  { // rounded cap at the tip
2220
  const tp = pts[n - 1], pp = pts[n - 2];
2221
  let dx = tp.x - pp.x, dy = tp.y - pp.y;
2222
- const dl = Math.sqrt(dx*dx + dy*dy) || 1;
2223
  const nx = -dy / dl, ny = dx / dl;
2224
  const hwT = w.tipW / 2;
2225
  ctx.quadraticCurveTo(tp.x + dx, tp.y + dy * 0.6, tp.x - nx * hwT, tp.y - ny * hwT);
@@ -2228,7 +2233,7 @@ function drawSeaweed() {
2228
  const p = pts[i];
2229
  const a = pts[Math.max(0, i - 1)], b = pts[Math.min(n - 1, i + 1)];
2230
  let dx = b.x - a.x, dy = b.y - a.y;
2231
- const dl = Math.sqrt(dx*dx + dy*dy) || 1;
2232
  const nx = -dy / dl, ny = dx / dl;
2233
  const f = i / (n - 1);
2234
  const hw = (w.width * (1 - f) + w.tipW * f) / 2;
@@ -2304,22 +2309,22 @@ function drawGrass(t) {
2304
  // organic rest-curve: each blade has its own natural bend (like seaweed)
2305
  const curveX = f * g.cA1 * Math.sin(f * g.cF1 + g.cP1);
2306
  // constant sway, amplified while rippling from a passing fish
2307
- const sway = fastSin(t * g.speed + g.phase + f * 2.4) * amp * f;
2308
  // v17: secondary wobble - finer inner movement for unique per-blade motion
2309
- const sWobble = fastSin(t * g.sFreq + g.sPh + f * 3.8) * g.sAmp * f;
2310
  pts.push({
2311
  x: g.x + g.lean * g.h * f + curveX + sway + sWobble,
2312
  y: g.y - g.h * f * (1 - g.ripple * 0.06)
2313
  });
2314
  }
2315
  // draw tapered outline around the physics points
2316
- ctx.fillStyle = g.fillStyle;
2317
  ctx.beginPath();
2318
  for (let i = 0; i < n; i++) {
2319
  const p = pts[i];
2320
  const a = pts[Math.max(0, i - 1)], b = pts[Math.min(n - 1, i + 1)];
2321
  let dx = b.x - a.x, dy = b.y - a.y;
2322
- const dl = Math.sqrt(dx*dx + dy*dy) || 1;
2323
  const nx = -dy / dl, ny = dx / dl; // perpendicular to local blade direction
2324
  const f = i / (n - 1);
2325
  const hw = (g.width * (1 - f) + 0.8 * f) / 2; // half-width: base → tip taper
@@ -2330,7 +2335,7 @@ function drawGrass(t) {
2330
  {
2331
  const tp = pts[n - 1], pp = pts[n - 2];
2332
  let dx = tp.x - pp.x, dy = tp.y - pp.y;
2333
- const dl = Math.sqrt(dx*dx + dy*dy) || 1;
2334
  const nx = -dy / dl, ny = dx / dl;
2335
  const hwT = 0.4; // tip half-width
2336
  ctx.quadraticCurveTo(tp.x + dx, tp.y + dy * 0.6, tp.x - nx * hwT, tp.y - ny * hwT);
@@ -2339,7 +2344,7 @@ function drawGrass(t) {
2339
  const p = pts[i];
2340
  const a = pts[Math.max(0, i - 1)], b = pts[Math.min(n - 1, i + 1)];
2341
  let dx = b.x - a.x, dy = b.y - a.y;
2342
- const dl = Math.sqrt(dx*dx + dy*dy) || 1;
2343
  const nx = -dy / dl, ny = dx / dl;
2344
  const f = i / (n - 1);
2345
  const hw = (g.width * (1 - f) + 0.8 * f) / 2;
@@ -2359,7 +2364,7 @@ function drawFgGrass(t) {
2359
  // around its base (same trick as the ultra-far seaweed). The baked blur hides the
2360
  // simplified motion, and this path is far cheaper than per-frame canvas filters.
2361
  if (g.sprite) {
2362
- const swayAngle = fastSin(t * g.speed + g.phase) * 0.05; // subtle: like farSeaweeds
2363
  ctx.save();
2364
  ctx.translate(g.x, g.y);
2365
  ctx.rotate(swayAngle);
@@ -2376,22 +2381,22 @@ function drawFgGrass(t) {
2376
  // organic rest-curve: each blade has its own natural bend
2377
  const curveX = f * g.cA1 * Math.sin(f * g.cF1 + g.cP1);
2378
  // constant sway, weighted toward the tip
2379
- const sway = fastSin(t * g.speed + g.phase + f * 2.4) * amp * f;
2380
  // v17: secondary wobble - finer inner movement for unique per-blade motion
2381
- const sWobble = fastSin(t * g.sFreq + g.sPh + f * 4.5) * g.sAmp * f;
2382
  pts.push({
2383
  x: g.x + g.lean * g.h * f + curveX + sway + sWobble,
2384
  y: g.y - g.h * f
2385
  });
2386
  }
2387
  // dark body of the blade (tapered outline)
2388
- ctx.fillStyle = g.fillStyle;
2389
  ctx.beginPath();
2390
  for (let i = 0; i < n; i++) {
2391
  const p = pts[i];
2392
  const a = pts[Math.max(0, i - 1)], b = pts[Math.min(n - 1, i + 1)];
2393
  let dx = b.x - a.x, dy = b.y - a.y;
2394
- const dl = Math.sqrt(dx*dx + dy*dy) || 1;
2395
  const nx = -dy / dl, ny = dx / dl;
2396
  const f = i / (n - 1);
2397
  const hw = (g.width * (1 - f) + 1.2 * f) / 2; // half-width: base → tip
@@ -2402,7 +2407,7 @@ function drawFgGrass(t) {
2402
  {
2403
  const tp = pts[n - 1], pp = pts[n - 2];
2404
  let dx = tp.x - pp.x, dy = tp.y - pp.y;
2405
- const dl = Math.sqrt(dx*dx + dy*dy) || 1;
2406
  const nx = -dy / dl, ny = dx / dl;
2407
  const hwT = 0.6; // tip half-width
2408
  ctx.quadraticCurveTo(tp.x + dx, tp.y + dy * 0.6, tp.x - nx * hwT, tp.y - ny * hwT);
@@ -2411,7 +2416,7 @@ function drawFgGrass(t) {
2411
  const p = pts[i];
2412
  const a = pts[Math.max(0, i - 1)], b = pts[Math.min(n - 1, i + 1)];
2413
  let dx = b.x - a.x, dy = b.y - a.y;
2414
- const dl = Math.sqrt(dx*dx + dy*dy) || 1;
2415
  const nx = -dy / dl, ny = dx / dl;
2416
  const f = i / (n - 1);
2417
  const hw = (g.width * (1 - f) + 1.2 * f) / 2;
@@ -2421,7 +2426,7 @@ function drawFgGrass(t) {
2421
  ctx.fill();
2422
 
2423
  // lighter inner highlight for depth: a thin centerline stroke
2424
- ctx.strokeStyle = g.strokeStyle;
2425
  ctx.lineWidth = g.width * 0.3;
2426
  ctx.lineCap = "round";
2427
  ctx.beginPath();
@@ -2720,7 +2725,7 @@ function drawLightPooling(t) {
2720
  for (const r of rays) {
2721
  // Compute where this ray hits the sand at y ≈ sandTop
2722
  // Use the same wave math as the ray drawing to match positions
2723
- const off = fastSin(t * r.speed * 6 + r.x) * 55;
2724
  // At the sand depth (vM near the bottom), compute the x position
2725
  const vM = 0.95; // approximate depth where sand starts
2726
  const wave =
@@ -2784,6 +2789,7 @@ function frame(now) {
2784
  const light = sampleLight(t);
2785
  drawBackground(light);
2786
  drawRays(t, light);
 
2787
  updateMotes(dt, t);
2788
 
2789
  // v5: ultra-far pre-rendered blurry seaweed — deepest layer, behind everything else
@@ -2883,7 +2889,7 @@ function frame(now) {
2883
  if (dx * dx + dy * dy < rr * rr) { b.dead = true; break; }
2884
  }
2885
  }
2886
- if (b.dead) { returnBubble(b); bubbles.splice(i, 1); continue; }
2887
  b.draw(ctx);
2888
  }
2889
 
 
88
  }
89
  let fishes = [], bubbles = [], seaweeds = [], backSeaweeds = [], midSeaweeds = [], farSeaweeds = [], motes = [], rays = [], rocks = [], grass = [], fgGrass = [];
90
  let playerFish = null; // the fish controlled by gamepad (spawned with F key)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  const GAMEPAD_DEADZONE = 0.15; // ignore small stick movements
92
  let curNight = 0; // v7: shared night factor (dims the top-light + sheen)
93
  let plantPoints = [], _ppUsed = 0; // collision points — a pooled set, reused in place
94
  let sandHeights = [];
95
  let sandParticles = []; // v22: grains of sand kicked up when a fast fish skims the sea floor
96
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  class Fish {
98
  static nextId = 0;
99
  constructor() {
 
366
  // Occasionally release a small cluster of bubbles from the mouth
367
  this.bubbleTimer -= dt;
368
  if (this.bubbleTimer <= 0) {
369
+ const mx = this.x + Math.cos(this.angle) * this.size * 0.62;
370
  const my = this.y + Math.sin(this.angle) * this.size * 0.62;
371
  const n = (2 + ((Math.random() * 3) | 0)) * 2; // 4..8 bubbles — doubled from the old 2..4
372
  for (let i = 0; i < n; i++) {
373
+ const bb = new Bubble(false, mx + rand(-3, 3), my + rand(-3, 3), this.size);
374
  bb.owner = this; // v10: a fish never pops its own bubbles
375
  bubbles.push(bb);
376
  }
 
382
 
383
  // Target sits ahead of the fish (in heading direction), offset by wanderTheta.
384
  // Steering toward it yields smooth meandering and genuine direction changes.
385
+ let wx = this.x + Math.cos(this.angle) * this.wanderDist + Math.cos(this.wanderTheta) * this.wanderRadius;
386
  let wy = this.y + Math.sin(this.angle) * this.wanderDist + Math.sin(this.wanderTheta) * this.wanderRadius;
387
 
388
  // v7: after dark, most fish bed down low in the grass; a few still hover by the seaweed.
 
391
  // v14: drift toward this fish's own sleep spot along the sand line —
392
  // spreads sleeping fish across the tank instead of one big clump
393
  wx += (this.sleepSpotX - wx) * night * 0.75;
394
+ const groundY = sandAt(this.x + Math.cos(this.angle) * this.wanderDist) - this.size * 0.5;
395
  wy += (groundY - wy) * night * 0.85;
396
  } else if (seaweeds.length) {
397
  // v9: each seaweed-hungry fish keeps to its own stalk instead of all crowding the nearest one
 
528
  if (inp.slow > 0) speedMult -= inp.slow * 0.85; // slow down
529
  speedMult = Math.max(0.15, Math.min(3.0, speedMult));
530
  const playerSpeed = this.speed * this.cruiseSpeed * speedMult * 60 * dt;
531
+ this.x += Math.cos(this.angle) * playerSpeed;
532
  this.y += Math.sin(this.angle) * playerSpeed;
533
  this.currentVelocity = this.speed * this.cruiseSpeed * speedMult * 60; // track actual velocity
534
  } else {
535
  // Normal AI fish movement.
536
  const sp = this.speed * this.cruiseSpeed * this.speedFactor * 60 * dt;
537
+ this.x += Math.cos(this.angle) * sp;
538
  this.y += Math.sin(this.angle) * sp;
539
  this.currentVelocity = this.speed * this.cruiseSpeed * this.speedFactor * 60; // track actual velocity
540
  }
 
565
  const v = Math.max(0, Math.min(1, this.viewAngle));
566
  let scaleY = 1 - 0.34 * v; // foreshorten toward the back view
567
  if (Math.abs(this.roll) > 0.01) { // banking roll still squashes a touch
568
+ scaleY *= Math.max(0.42, Math.cos(this.roll));
569
  c.rotate(this.roll * 0.1);
570
  }
571
  c.scale(1, scaleY);
 
580
  // traveling body wave
581
  const waveK = Math.PI / s;
582
  const waveAmp = s * 0.12;
583
+ const sway = Math.sin(-s * waveK + this.phase) * s * 0.34;
584
 
585
  // Top/bottom view: body seen from above or below
586
  // Dorsal fin visible as a line down the center when viewed from above
 
665
  const waveK = Math.PI / s; // ~one wavelength across the body length
666
  const waveAmp = s * 0.12; // subtle flex amplitude
667
  // tail fin follows the body wave at its base, amplified (same phase → no fighting)
668
+ const sway = Math.sin(-s * waveK + this.phase) * s * 0.34;
669
 
670
  // v24: Ecco-style blend factor (0 = side profile, 1 = seen from above/below).
671
  // Flattens the tail lobes and spreads eyes/barbels as we see more of the back.
 
726
  const bx = rootX - L * (0.25 + 0.65 * spd) * t;
727
  const by = rootY + side * L * (0.42 + 0.3 * ev) * t * t;
728
  const amp = s * 0.17 * t * (0.35 + 0.65 * spd);
729
+ const off = Math.sin(t * 5.5 - this.phase * 1.4 + this.whiskPh) * amp;
730
  c.lineTo(bx, by + off);
731
  }
732
  c.stroke();
 
792
  // left edge, top → bottom: bend the centerline + taper the width with t
793
  for (let j = 0; j <= N; j++) {
794
  const t = j / N;
795
+ const cx = sx + Math.sin(t * st.cFreq * Math.PI + st.cPh) * st.cAmp * s;
796
+ const hwL = hw0 * (1 + st.tAmp * Math.sin(t * st.tFreq * Math.PI + st.tPh));
797
  const xL = cx - hwL, yL = wy(xL) + (t - 0.5) * H;
798
  if (j === 0) c.moveTo(xL, yL); else c.lineTo(xL, yL);
799
  }
800
  // right edge, bottom → top
801
  for (let j = N; j >= 0; j--) {
802
  const t = j / N;
803
+ const cx = sx + Math.sin(t * st.cFreq * Math.PI + st.cPh) * st.cAmp * s;
804
+ const hwR = hw0 * (1 + st.tAmp * Math.sin(t * st.tFreq * Math.PI + st.tPh));
805
  const xR = cx + hwR, yR = wy(xR) + (t - 0.5) * H;
806
  c.lineTo(xR, yR);
807
  }
 
948
  const bx = rootX - L * (0.25 + 0.65 * spd) * t;
949
  const by = rootY + side * L * (0.42 + 0.3 * ev) * t * t;
950
  const amp = s * 0.17 * t * (0.35 + 0.65 * spd);
951
+ const off = Math.sin(t * 5.5 - this.phase * 1.4 + this.whiskPh) * amp;
952
  c.lineTo(bx, by + off);
953
  }
954
  c.stroke();
 
1011
  }
1012
  }
1013
  reset(init) {
 
1014
  this.x = rand(0, W);
1015
  this.y = init ? rand(H * 0.3, H) : H + rand(10, 80);
1016
  this.r = rand(2, 7);
 
1021
  }
1022
  update(dt, t) {
1023
  this.y -= this.speed * dt;
1024
+ this.x += Math.sin(t * 1.6 + this.p) * 22 * dt;
1025
  if (this.y < -12) { if (this.isMouth) this.dead = true; else this.reset(false); }
1026
  }
1027
  draw(c) {
 
1336
  sPh: rand(0, Math.PI * 2),
1337
  // PERF: per-blade scratch points — drawGrass reuses them instead of
1338
  // allocating a fresh pts array + point objects every frame (GC churn)
1339
+ pts: Array.from({ length: bladeSegs + 1 }, () => ({ x: 0, y: 0 }))
1340
+ });
 
1341
  }
1342
  }
1343
 
 
1377
  // v28: ~40% of blades get a gentle blur (pre-rendered sprite) for depth
1378
  blurred: Math.random() < 0.4,
1379
  sprite: null,
1380
+ spriteScale: 1
1381
+ });
 
1382
  }
1383
  }
1384
  buildFgGrassSprites(); // v28: bake the defocused-blade sprites once (free at runtime)
 
1393
  for (let i = 0; i < nFish; i++) fishes.push(new Fish());
1394
 
1395
  const nAmbient = Math.round(26 * 0.75); // v10: ambient bubbles reduced 25% (fish production unchanged)
1396
+ for (let i = 0; i < nAmbient; i++) bubbles.push(new Bubble(true));
1397
 
1398
  for (let i = 0; i < 45; i++) {
1399
  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) });
 
1651
  applyRayTint(); // cheap re-tint of the cached blurred sprites (no blur re-run)
1652
  }
1653
 
1654
+
1655
+
1656
+ ctx.restore();
1657
  for (const r of rays) {
1658
  // blit horizontal slices with the undulation offset at each depth — same wave
1659
  // math as before, so the beams shimmer exactly like they always have
1660
+ const off = Math.sin(t * r.speed * 6 + r.x) * 55; // slow drift of the whole beam
1661
  for (let i = 0; i < RAY_SLICES; i++) {
1662
  const vT = i / RAY_SLICES, vB = (i + 1) / RAY_SLICES, vM = (vT + vB) / 2;
1663
  const ya = yTop0 + vT * ySpan, yb = yTop0 + vB * ySpan;
 
1673
  }
1674
  }
1675
 
1676
+ // v29: Fish occlude godrays - cast long blurred shadows down to the tank floor
1677
+ function drawFishShadows(t) {
1678
+ ctx.save();
1679
+ ctx.globalCompositeOperation = 'multiply';
1680
+ for (const f of fishes) {
1681
+ // Fish blocks light, casting a shadow that extends down to the sand
1682
+ const shadowTop = f.y - f.size * 0.3;
1683
+ const shadowBottom = sandAt(f.x) - 5;
1684
+ const shadowHeight = shadowBottom - shadowTop;
1685
+
1686
+ if (shadowHeight > 0) {
1687
+ // Shadow width tapers from fish size at top to wider at bottom
1688
+ const topWidth = f.size * 1.2;
1689
+ const bottomWidth = f.size * 2.5;
1690
+
1691
+ // Create gradient for shadow (darker near fish, fading downward)
1692
+ const g = ctx.createLinearGradient(f.x, shadowTop, f.x, shadowBottom);
1693
+ g.addColorStop(0, 'rgba(0,0,0,0.22)');
1694
+ g.addColorStop(0.3, 'rgba(0,0,0,0.12)');
1695
+ g.addColorStop(1, 'rgba(0,0,0,0.03)');
1696
+
1697
+ ctx.fillStyle = g;
1698
+ ctx.beginPath();
1699
+ // Draw tapered shadow shape with soft edges using quadratic curves
1700
+ const blur = f.size * 0.4;
1701
+ ctx.moveTo(f.x - topWidth/2, shadowTop + blur);
1702
+ ctx.quadraticCurveTo(f.x - topWidth/2, shadowTop, f.x, shadowTop);
1703
+ ctx.quadraticCurveTo(f.x + topWidth/2, shadowTop, f.x + topWidth/2, shadowTop + blur);
1704
+ ctx.lineTo(f.x + bottomWidth/2, shadowBottom - blur);
1705
+ ctx.quadraticCurveTo(f.x + bottomWidth/2, shadowBottom, f.x, shadowBottom);
1706
+ ctx.quadraticCurveTo(f.x - bottomWidth/2, shadowBottom, f.x - bottomWidth/2, shadowBottom - blur);
1707
+ ctx.closePath();
1708
+ ctx.fill();
1709
+ }
1710
+ }
1711
+ ctx.restore();
1712
+ }
1713
+
1714
+
1715
+
1716
+
1717
  function updateMotes(dt, t) {
1718
  ctx.fillStyle = "rgba(220,240,255,0.3)";
1719
  for (const m of motes) {
 
1766
 
1767
  // PERF: shared sway function (was a per-stalk closure recreated every frame)
1768
  function stalkSwayX(w, t, f) {
1769
+ return Math.sin(t * w.waveF1 + w.wavePh1 + f * 2.4) * w.waveA1 * f * f +
1770
+ Math.sin(t * w.waveF2 + w.wavePh2 + f * 3.8) * w.waveA2 * f * f +
1771
+ Math.sin(t * w.waveF3 + w.wavePh3 + f * 5.4) * w.waveA3 * f;
1772
  }
1773
 
1774
  // v4.3: draw background seaweed procedurally — multi-wave organic motion instead of
 
1828
  for (const b of w.branches) {
1829
  const idx = Math.min(n, Math.max(1, Math.round(b.f * n)));
1830
  const ax = pxs[idx], ay = pys[idx];
1831
+ const flutter = Math.sin(t * w.waveF2 * 1.4 + b.ph) * (3 + w.waveA2 * 0.4);
1832
  const bx = ax + b.side * b.len + flutter;
1833
  const by = ay - b.up;
1834
  ctx.beginPath();
 
1900
  for (const b of w.branches) {
1901
  const idx = Math.min(n, Math.max(1, Math.round(b.f * n)));
1902
  const ax = pxs[idx], ay = pys[idx];
1903
+ const flutter = Math.sin(t * w.waveF2 * 1.4 + b.ph) * (3 + w.waveA2 * 0.4);
1904
  const bx = ax + b.side * b.len + flutter;
1905
  const by = ay - b.up;
1906
  ctx.beginPath();
 
2091
 
2092
  const baseY = sandAt(w.x) + 10;
2093
  // Simple rotational sway - root stays planted, sprite rotates slightly
2094
+ const swayAngle = Math.sin(t * w.speed + w.phase) * 0.04; // very subtle rotation
2095
 
2096
  ctx.save();
2097
  // Translate to the base point (where the stalk meets the sand)
 
2117
  const moveF = Math.min(1, fSpd / 1.2); // v9: how actively this fish is swimming
2118
  for (let i = 1; i <= w.segs; i++) {
2119
  const fl = i / w.segs;
2120
+ const px = w.x + w.off[i] + stalkCurveX(w, fl) + Math.sin(t * w.speed + w.phase + fl * 2.6) * fl * fl * 24;
2121
  const py = sandAt(w.x) + 8 - w.h * fl;
2122
  const dx = px - f.x, dy = py - f.y;
2123
  const d2 = dx * dx + dy * dy;
 
2159
  for (let i = 0; i <= w.segs; i++) {
2160
  const f = i / w.segs;
2161
  // normal sway + a fast extra flinch while being pushed
2162
+ const sway = Math.sin(t * w.speed + w.phase + f * 2.6) * f * f * 24
2163
+ + Math.sin(t * 7 + w.phase) * w.touch * f * 16;
2164
  // v17: secondary wobble - a finer inner movement for unique per-stalk motion
2165
+ const sWobble = Math.sin(t * w.sFreq + w.sPh + f * 4.2) * w.sAmp * f;
2166
  const px = w.x + (w.off[i] || 0) + stalkCurveX(w, f) + sway + sWobble;
2167
  const py = baseY - w.h * f;
2168
  w.points.push({ x: px, y: py });
 
2170
  }
2171
  for (const b of w.branches) {
2172
  const a = w.points[Math.min(w.segs, Math.max(1, Math.round(b.f * w.segs)))];
2173
+ const flutter = Math.sin(t * w.speed * 1.4 + b.ph) * (3 + w.touch * 9);
2174
  b.tx = a.x + b.side * b.len + flutter;
2175
  b.ty = a.y - b.up;
2176
  plantPoints.push({ x: b.tx, y: b.ty, r: 5, s: w });
 
2214
  const p = pts[i];
2215
  const a = pts[Math.max(0, i - 1)], b = pts[Math.min(n - 1, i + 1)];
2216
  let dx = b.x - a.x, dy = b.y - a.y;
2217
+ const dl = Math.hypot(dx, dy) || 1;
2218
  const nx = -dy / dl, ny = dx / dl; // perpendicular to the local stalk direction
2219
  const f = i / (n - 1);
2220
  const hw = (w.width * (1 - f) + w.tipW * f) / 2; // half-width: base → tip taper
 
2224
  { // rounded cap at the tip
2225
  const tp = pts[n - 1], pp = pts[n - 2];
2226
  let dx = tp.x - pp.x, dy = tp.y - pp.y;
2227
+ const dl = Math.hypot(dx, dy) || 1;
2228
  const nx = -dy / dl, ny = dx / dl;
2229
  const hwT = w.tipW / 2;
2230
  ctx.quadraticCurveTo(tp.x + dx, tp.y + dy * 0.6, tp.x - nx * hwT, tp.y - ny * hwT);
 
2233
  const p = pts[i];
2234
  const a = pts[Math.max(0, i - 1)], b = pts[Math.min(n - 1, i + 1)];
2235
  let dx = b.x - a.x, dy = b.y - a.y;
2236
+ const dl = Math.hypot(dx, dy) || 1;
2237
  const nx = -dy / dl, ny = dx / dl;
2238
  const f = i / (n - 1);
2239
  const hw = (w.width * (1 - f) + w.tipW * f) / 2;
 
2309
  // organic rest-curve: each blade has its own natural bend (like seaweed)
2310
  const curveX = f * g.cA1 * Math.sin(f * g.cF1 + g.cP1);
2311
  // constant sway, amplified while rippling from a passing fish
2312
+ const sway = Math.sin(t * g.speed + g.phase + f * 2.4) * amp * f;
2313
  // v17: secondary wobble - finer inner movement for unique per-blade motion
2314
+ const sWobble = Math.sin(t * g.sFreq + g.sPh + f * 3.8) * g.sAmp * f;
2315
  pts.push({
2316
  x: g.x + g.lean * g.h * f + curveX + sway + sWobble,
2317
  y: g.y - g.h * f * (1 - g.ripple * 0.06)
2318
  });
2319
  }
2320
  // draw tapered outline around the physics points
2321
+ ctx.fillStyle = `hsla(${g.hue},58%,40%,0.92)`;
2322
  ctx.beginPath();
2323
  for (let i = 0; i < n; i++) {
2324
  const p = pts[i];
2325
  const a = pts[Math.max(0, i - 1)], b = pts[Math.min(n - 1, i + 1)];
2326
  let dx = b.x - a.x, dy = b.y - a.y;
2327
+ const dl = Math.hypot(dx, dy) || 1;
2328
  const nx = -dy / dl, ny = dx / dl; // perpendicular to local blade direction
2329
  const f = i / (n - 1);
2330
  const hw = (g.width * (1 - f) + 0.8 * f) / 2; // half-width: base → tip taper
 
2335
  {
2336
  const tp = pts[n - 1], pp = pts[n - 2];
2337
  let dx = tp.x - pp.x, dy = tp.y - pp.y;
2338
+ const dl = Math.hypot(dx, dy) || 1;
2339
  const nx = -dy / dl, ny = dx / dl;
2340
  const hwT = 0.4; // tip half-width
2341
  ctx.quadraticCurveTo(tp.x + dx, tp.y + dy * 0.6, tp.x - nx * hwT, tp.y - ny * hwT);
 
2344
  const p = pts[i];
2345
  const a = pts[Math.max(0, i - 1)], b = pts[Math.min(n - 1, i + 1)];
2346
  let dx = b.x - a.x, dy = b.y - a.y;
2347
+ const dl = Math.hypot(dx, dy) || 1;
2348
  const nx = -dy / dl, ny = dx / dl;
2349
  const f = i / (n - 1);
2350
  const hw = (g.width * (1 - f) + 0.8 * f) / 2;
 
2364
  // around its base (same trick as the ultra-far seaweed). The baked blur hides the
2365
  // simplified motion, and this path is far cheaper than per-frame canvas filters.
2366
  if (g.sprite) {
2367
+ const swayAngle = Math.sin(t * g.speed + g.phase) * 0.05; // subtle: like farSeaweeds
2368
  ctx.save();
2369
  ctx.translate(g.x, g.y);
2370
  ctx.rotate(swayAngle);
 
2381
  // organic rest-curve: each blade has its own natural bend
2382
  const curveX = f * g.cA1 * Math.sin(f * g.cF1 + g.cP1);
2383
  // constant sway, weighted toward the tip
2384
+ const sway = Math.sin(t * g.speed + g.phase + f * 2.4) * amp * f;
2385
  // v17: secondary wobble - finer inner movement for unique per-blade motion
2386
+ const sWobble = Math.sin(t * g.sFreq + g.sPh + f * 4.5) * g.sAmp * f;
2387
  pts.push({
2388
  x: g.x + g.lean * g.h * f + curveX + sway + sWobble,
2389
  y: g.y - g.h * f
2390
  });
2391
  }
2392
  // dark body of the blade (tapered outline)
2393
+ ctx.fillStyle = `hsla(${g.hue},60%,31%,0.96)`;
2394
  ctx.beginPath();
2395
  for (let i = 0; i < n; i++) {
2396
  const p = pts[i];
2397
  const a = pts[Math.max(0, i - 1)], b = pts[Math.min(n - 1, i + 1)];
2398
  let dx = b.x - a.x, dy = b.y - a.y;
2399
+ const dl = Math.hypot(dx, dy) || 1;
2400
  const nx = -dy / dl, ny = dx / dl;
2401
  const f = i / (n - 1);
2402
  const hw = (g.width * (1 - f) + 1.2 * f) / 2; // half-width: base → tip
 
2407
  {
2408
  const tp = pts[n - 1], pp = pts[n - 2];
2409
  let dx = tp.x - pp.x, dy = tp.y - pp.y;
2410
+ const dl = Math.hypot(dx, dy) || 1;
2411
  const nx = -dy / dl, ny = dx / dl;
2412
  const hwT = 0.6; // tip half-width
2413
  ctx.quadraticCurveTo(tp.x + dx, tp.y + dy * 0.6, tp.x - nx * hwT, tp.y - ny * hwT);
 
2416
  const p = pts[i];
2417
  const a = pts[Math.max(0, i - 1)], b = pts[Math.min(n - 1, i + 1)];
2418
  let dx = b.x - a.x, dy = b.y - a.y;
2419
+ const dl = Math.hypot(dx, dy) || 1;
2420
  const nx = -dy / dl, ny = dx / dl;
2421
  const f = i / (n - 1);
2422
  const hw = (g.width * (1 - f) + 1.2 * f) / 2;
 
2426
  ctx.fill();
2427
 
2428
  // lighter inner highlight for depth: a thin centerline stroke
2429
+ ctx.strokeStyle = `hsla(${g.hue + 6},64%,45%,0.5)`;
2430
  ctx.lineWidth = g.width * 0.3;
2431
  ctx.lineCap = "round";
2432
  ctx.beginPath();
 
2725
  for (const r of rays) {
2726
  // Compute where this ray hits the sand at y ≈ sandTop
2727
  // Use the same wave math as the ray drawing to match positions
2728
+ const off = Math.sin(t * r.speed * 6 + r.x) * 55;
2729
  // At the sand depth (vM near the bottom), compute the x position
2730
  const vM = 0.95; // approximate depth where sand starts
2731
  const wave =
 
2789
  const light = sampleLight(t);
2790
  drawBackground(light);
2791
  drawRays(t, light);
2792
+ drawFishShadows(t);
2793
  updateMotes(dt, t);
2794
 
2795
  // v5: ultra-far pre-rendered blurry seaweed — deepest layer, behind everything else
 
2889
  if (dx * dx + dy * dy < rr * rr) { b.dead = true; break; }
2890
  }
2891
  }
2892
+ if (b.dead) { bubbles.splice(i, 1); continue; }
2893
  b.draw(ctx);
2894
  }
2895