Upload index.html
Browse files- 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 +
|
| 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 =
|
| 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 +
|
| 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 +
|
| 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 +=
|
| 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 +=
|
| 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,
|
| 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 =
|
| 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 =
|
| 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 =
|
| 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 +
|
| 832 |
-
const hwL = hw0 * (1 + st.tAmp *
|
| 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 +
|
| 840 |
-
const hwR = hw0 * (1 + st.tAmp *
|
| 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 =
|
| 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 +=
|
| 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 |
-
|
| 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 |
-
|
| 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++)
|
| 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 =
|
| 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
|
| 1765 |
-
|
| 1766 |
-
|
| 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 =
|
| 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 =
|
| 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 =
|
| 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) +
|
| 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 =
|
| 2158 |
-
+
|
| 2159 |
// v17: secondary wobble - a finer inner movement for unique per-stalk motion
|
| 2160 |
-
const sWobble =
|
| 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 =
|
| 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.
|
| 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.
|
| 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.
|
| 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 =
|
| 2308 |
// v17: secondary wobble - finer inner movement for unique per-blade motion
|
| 2309 |
-
const sWobble =
|
| 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.
|
| 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.
|
| 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.
|
| 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.
|
| 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 =
|
| 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 =
|
| 2380 |
// v17: secondary wobble - finer inner movement for unique per-blade motion
|
| 2381 |
-
const sWobble =
|
| 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.
|
| 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.
|
| 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.
|
| 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.
|
| 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.
|
| 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 =
|
| 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) {
|
| 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 |
|