Upload index.html
Browse files- index.html +87 -45
index.html
CHANGED
|
@@ -645,22 +645,6 @@ class Fish {
|
|
| 645 |
c.closePath();
|
| 646 |
c.fill();
|
| 647 |
|
| 648 |
-
// pectoral fin (flapping)
|
| 649 |
-
const flap = Math.sin(this.phase * 1.3) * 0.45;
|
| 650 |
-
c.save();
|
| 651 |
-
c.translate(s * 0.05, s * 0.12);
|
| 652 |
-
c.rotate(0.7 + flap);
|
| 653 |
-
// v17: pectoral fin matches the body color (no more blue arms on silver fish), but
|
| 654 |
-
// gets a thin darker outline so it stays visible sitting on top of the same-colored body
|
| 655 |
-
c.fillStyle = this.pevFillStyle;
|
| 656 |
-
c.beginPath();
|
| 657 |
-
c.ellipse(0, 0, s * 0.3, s * 0.14, 0, 0, Math.PI * 2);
|
| 658 |
-
c.fill();
|
| 659 |
-
c.strokeStyle = this.pevStrokeStyle;
|
| 660 |
-
c.lineWidth = Math.max(1, s * 0.03);
|
| 661 |
-
c.stroke();
|
| 662 |
-
c.restore();
|
| 663 |
-
|
| 664 |
// eye
|
| 665 |
c.fillStyle = "#fff";
|
| 666 |
c.beginPath();
|
|
@@ -709,37 +693,71 @@ class Fish {
|
|
| 709 |
|
| 710 |
c.restore();
|
| 711 |
|
| 712 |
-
//
|
| 713 |
-
//
|
| 714 |
{
|
| 715 |
const ca = Math.cos(this.angle), sa = Math.sin(this.angle);
|
| 716 |
-
|
| 717 |
-
const
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
const ly = -s * 0.42 * Math.sin(th); // across body (local up)
|
| 722 |
-
pts.push([this.x + lx*ca - ly*sa, this.y + lx*sa + ly*ca]);
|
| 723 |
-
}
|
| 724 |
-
let ti = 0; for (let i = 1; i < N; i++) if (pts[i][1] < pts[ti][1]) ti = i;
|
| 725 |
-
const arc = [];
|
| 726 |
-
for (let k = -8; k <= 8; k++) {
|
| 727 |
-
const idx = ((ti + k) % N + N) % N;
|
| 728 |
-
arc.push(pts[idx]);
|
| 729 |
-
}
|
| 730 |
const a = 0.35 * (1 - curNight * 0.65); // dimmer at night, still present
|
| 731 |
if (a > 0.02) {
|
| 732 |
c.save();
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 740 |
c.restore();
|
| 741 |
}
|
| 742 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 743 |
}
|
| 744 |
}
|
| 745 |
|
|
@@ -857,6 +875,10 @@ function buildScene() {
|
|
| 857 |
// so fish push the stalk where it actually looks. Weighted by f β root stays planted.
|
| 858 |
cA1: rand(5, 12), cF1: rand(0.9, 1.8), cP1: rand(0, Math.PI * 2),
|
| 859 |
cA2: rand(2, 6), cF2: rand(2.2, 3.6), cP2: rand(0, Math.PI * 2),
|
|
|
|
|
|
|
|
|
|
|
|
|
| 860 |
tipW: rand(1.4, 2.6), // v15: blade tapers from width β tipW up the stipe
|
| 861 |
// PERF: stroke styles precomputed once (hue is fixed per stalk) β drawSeaweed
|
| 862 |
// used to rebuild both template strings every frame for every stalk.
|
|
@@ -922,7 +944,11 @@ function buildScene() {
|
|
| 922 |
cA1: rand(-5, 5), // primary bend amplitude
|
| 923 |
cF1: rand(2.2, 4.5), // frequency along blade length
|
| 924 |
cP1: rand(0, Math.PI * 2), // phase offset
|
| 925 |
-
segs: 3 + ((Math.random() * 2) | 0) // 3-4 segments for tapered outline
|
|
|
|
|
|
|
|
|
|
|
|
|
| 926 |
});
|
| 927 |
}
|
| 928 |
}
|
|
@@ -951,7 +977,11 @@ function buildScene() {
|
|
| 951 |
cA1: rand(-8, 8), // primary bend amplitude (larger for foreground)
|
| 952 |
cF1: rand(2, 4), // frequency along blade length
|
| 953 |
cP1: rand(0, Math.PI * 2), // phase offset
|
| 954 |
-
segs: 4 + ((Math.random() * 2) | 0) // 4-5 segments for tapered outline
|
|
|
|
|
|
|
|
|
|
|
|
|
| 955 |
});
|
| 956 |
}
|
| 957 |
}
|
|
@@ -967,9 +997,12 @@ function buildScene() {
|
|
| 967 |
}
|
| 968 |
|
| 969 |
for (let i = 0; i < 5; i++) {
|
|
|
|
|
|
|
|
|
|
| 970 |
rays.push({
|
| 971 |
x: rand(0, W), w: rand(80, 210), tilt: rand(-0.35, 0.35),
|
| 972 |
-
speed: rand(0.06, 0.14) * (Math.random() < 0.5 ? 1 : -1), alpha: rand(0.04, 0.09),
|
| 973 |
// undulation β two traveling waves so each beam shimmers like light refracted through moving water
|
| 974 |
uAmp: rand(18, 46), uLen: rand(2.2, 4.5), uSpd: rand(0.35, 0.9) * (Math.random() < 0.5 ? 1 : -1), uPh: rand(0, Math.PI * 2),
|
| 975 |
vAmp: rand(6, 14), vLen: rand(5, 9), vSpd: rand(0.8, 1.6) * (Math.random() < 0.5 ? 1 : -1), vPh: rand(0, Math.PI * 2)
|
|
@@ -1079,6 +1112,8 @@ function drawRays(t, light) {
|
|
| 1079 |
const [rr, rg, rb] = light.ray;
|
| 1080 |
const segs = RAY_SEGS;
|
| 1081 |
for (const r of rays) {
|
|
|
|
|
|
|
| 1082 |
// slow horizontal drift of the whole beam (kept from v1)
|
| 1083 |
const off = Math.sin(t * r.speed * 6 + r.x) * 55;
|
| 1084 |
const x0 = r.x + off;
|
|
@@ -1130,6 +1165,7 @@ function drawRays(t, light) {
|
|
| 1130 |
RC[i][0] = cxm + coreHalf; RC[i][1] = ry2;
|
| 1131 |
}
|
| 1132 |
band(LC, RC, r.alpha * 1.5, 0.65, 0);
|
|
|
|
| 1133 |
}
|
| 1134 |
}
|
| 1135 |
|
|
@@ -1238,7 +1274,9 @@ function updatePlants(t, dt) {
|
|
| 1238 |
// normal sway + a fast extra flinch while being pushed
|
| 1239 |
const sway = Math.sin(t * w.speed + w.phase + f * 2.6) * f * f * 24
|
| 1240 |
+ Math.sin(t * 7 + w.phase) * w.touch * f * 16;
|
| 1241 |
-
|
|
|
|
|
|
|
| 1242 |
const py = baseY - w.h * f;
|
| 1243 |
w.points.push({ x: px, y: py });
|
| 1244 |
plantPoints.push({ x: px, y: py, r: w.width * 0.55, s: w });
|
|
@@ -1383,8 +1421,10 @@ function drawGrass(t) {
|
|
| 1383 |
const curveX = f * g.cA1 * Math.sin(f * g.cF1 + g.cP1);
|
| 1384 |
// constant sway, amplified while rippling from a passing fish
|
| 1385 |
const sway = Math.sin(t * g.speed + g.phase + f * 2.4) * amp * f;
|
|
|
|
|
|
|
| 1386 |
pts.push({
|
| 1387 |
-
x: g.x + g.lean * g.h * f + curveX + sway,
|
| 1388 |
y: g.y - g.h * f * (1 - g.ripple * 0.06)
|
| 1389 |
});
|
| 1390 |
}
|
|
@@ -1441,8 +1481,10 @@ function drawFgGrass(t) {
|
|
| 1441 |
const curveX = f * g.cA1 * Math.sin(f * g.cF1 + g.cP1);
|
| 1442 |
// constant sway, weighted toward the tip
|
| 1443 |
const sway = Math.sin(t * g.speed + g.phase + f * 2.4) * amp * f;
|
|
|
|
|
|
|
| 1444 |
pts.push({
|
| 1445 |
-
x: g.x + g.lean * g.h * f + curveX + sway,
|
| 1446 |
y: g.y - g.h * f
|
| 1447 |
});
|
| 1448 |
}
|
|
|
|
| 645 |
c.closePath();
|
| 646 |
c.fill();
|
| 647 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 648 |
// eye
|
| 649 |
c.fillStyle = "#fff";
|
| 650 |
c.beginPath();
|
|
|
|
| 693 |
|
| 694 |
c.restore();
|
| 695 |
|
| 696 |
+
// v17: overhead light β a soft elliptical highlight matching the fish body shape,
|
| 697 |
+
// positioned on the upper edge and rotated with the fish's angle.
|
| 698 |
{
|
| 699 |
const ca = Math.cos(this.angle), sa = Math.sin(this.angle);
|
| 700 |
+
// Find which side faces up (screen space)
|
| 701 |
+
const upY = -sa; // screen-space y-component of the fish's local "up" direction
|
| 702 |
+
// Smooth transition instead of hard switch to prevent flickering/snapping
|
| 703 |
+
const sideUp = -upY / (Math.abs(upY) + 0.15);
|
| 704 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 705 |
const a = 0.35 * (1 - curNight * 0.65); // dimmer at night, still present
|
| 706 |
if (a > 0.02) {
|
| 707 |
c.save();
|
| 708 |
+
// Position the ellipse on the upper edge of the fish body
|
| 709 |
+
const offsetX = sideUp * s * 0.18; // offset along the fish's local up axis
|
| 710 |
+
const lightX = this.x + offsetX * ca;
|
| 711 |
+
const lightY = this.y + offsetX * sa;
|
| 712 |
+
|
| 713 |
+
// Draw two concentric ellipses matching the fish body proportions
|
| 714 |
+
c.translate(lightX, lightY);
|
| 715 |
+
c.rotate(this.angle);
|
| 716 |
+
|
| 717 |
+
// Outer soft glow β larger and more transparent
|
| 718 |
+
c.fillStyle = `rgba(255,255,255,${(a * 0.3).toFixed(3)})`;
|
| 719 |
+
c.beginPath();
|
| 720 |
+
c.ellipse(0, 0, s * 0.65, s * 0.25, 0, 0, Math.PI * 2);
|
| 721 |
+
c.fill();
|
| 722 |
+
|
| 723 |
+
// Inner bright core β smaller and more opaque
|
| 724 |
+
c.fillStyle = `rgba(255,255,255,${a.toFixed(3)})`;
|
| 725 |
+
c.beginPath();
|
| 726 |
+
c.ellipse(0, 0, s * 0.35, s * 0.1, 0, 0, Math.PI * 2);
|
| 727 |
+
c.fill();
|
| 728 |
+
|
| 729 |
c.restore();
|
| 730 |
}
|
| 731 |
}
|
| 732 |
+
// v19: pectoral fins drawn OVER the reflections for correct layering
|
| 733 |
+
{
|
| 734 |
+
c.save();
|
| 735 |
+
c.translate(this.x, this.y);
|
| 736 |
+
c.rotate(this.angle);
|
| 737 |
+
if (Math.abs(this.roll) > 0.01) {
|
| 738 |
+
c.scale(1, Math.max(0.42, Math.cos(this.roll)));
|
| 739 |
+
c.rotate(this.roll * 0.1);
|
| 740 |
+
}
|
| 741 |
+
|
| 742 |
+
// pectoral fin (flapping)
|
| 743 |
+
const flap = Math.sin(this.phase * 1.3) * 0.45;
|
| 744 |
+
c.save();
|
| 745 |
+
c.translate(s * 0.05, s * 0.12);
|
| 746 |
+
c.rotate(0.7 + flap);
|
| 747 |
+
// v17: pectoral fin matches the body color (no more blue arms on silver fish), but
|
| 748 |
+
// gets a thin darker outline so it stays visible sitting on top of the same-colored body
|
| 749 |
+
c.fillStyle = this.pevFillStyle;
|
| 750 |
+
c.beginPath();
|
| 751 |
+
c.ellipse(0, 0, s * 0.3, s * 0.14, 0, 0, Math.PI * 2);
|
| 752 |
+
c.fill();
|
| 753 |
+
c.strokeStyle = this.pevStrokeStyle;
|
| 754 |
+
c.lineWidth = Math.max(1, s * 0.03);
|
| 755 |
+
c.stroke();
|
| 756 |
+
c.restore();
|
| 757 |
+
|
| 758 |
+
c.restore();
|
| 759 |
+
}
|
| 760 |
+
|
| 761 |
}
|
| 762 |
}
|
| 763 |
|
|
|
|
| 875 |
// so fish push the stalk where it actually looks. Weighted by f β root stays planted.
|
| 876 |
cA1: rand(5, 12), cF1: rand(0.9, 1.8), cP1: rand(0, Math.PI * 2),
|
| 877 |
cA2: rand(2, 6), cF2: rand(2.2, 3.6), cP2: rand(0, Math.PI * 2),
|
| 878 |
+
// v17: secondary wobble - a finer inner shimmer for unique motion per stalk
|
| 879 |
+
sAmp: rand(2, 5), // amplitude of the secondary wave
|
| 880 |
+
sFreq: rand(3.5, 6.5), // higher frequency than primary sway
|
| 881 |
+
sPh: rand(0, Math.PI * 2), // phase offset
|
| 882 |
tipW: rand(1.4, 2.6), // v15: blade tapers from width β tipW up the stipe
|
| 883 |
// PERF: stroke styles precomputed once (hue is fixed per stalk) β drawSeaweed
|
| 884 |
// used to rebuild both template strings every frame for every stalk.
|
|
|
|
| 944 |
cA1: rand(-5, 5), // primary bend amplitude
|
| 945 |
cF1: rand(2.2, 4.5), // frequency along blade length
|
| 946 |
cP1: rand(0, Math.PI * 2), // phase offset
|
| 947 |
+
segs: 3 + ((Math.random() * 2) | 0), // 3-4 segments for tapered outline
|
| 948 |
+
// v17: secondary wobble - finer inner shimmer per blade
|
| 949 |
+
sAmp: rand(1.5, 3.5),
|
| 950 |
+
sFreq: rand(4.5, 8),
|
| 951 |
+
sPh: rand(0, Math.PI * 2)
|
| 952 |
});
|
| 953 |
}
|
| 954 |
}
|
|
|
|
| 977 |
cA1: rand(-8, 8), // primary bend amplitude (larger for foreground)
|
| 978 |
cF1: rand(2, 4), // frequency along blade length
|
| 979 |
cP1: rand(0, Math.PI * 2), // phase offset
|
| 980 |
+
segs: 4 + ((Math.random() * 2) | 0), // 4-5 segments for tapered outline
|
| 981 |
+
// v17: secondary wobble - finer inner shimmer per blade (foreground)
|
| 982 |
+
sAmp: rand(2, 4.5),
|
| 983 |
+
sFreq: rand(3.8, 6.5),
|
| 984 |
+
sPh: rand(0, Math.PI * 2)
|
| 985 |
});
|
| 986 |
}
|
| 987 |
}
|
|
|
|
| 997 |
}
|
| 998 |
|
| 999 |
for (let i = 0; i < 5; i++) {
|
| 1000 |
+
// random blur state: 0px (as-is), ~3px (semi-blurry), or ~7px (blurry) for visual variety
|
| 1001 |
+
const blurRoll = Math.random();
|
| 1002 |
+
const blur = blurRoll < 0.34 ? 0 : (blurRoll < 0.67 ? 3 : 7);
|
| 1003 |
rays.push({
|
| 1004 |
x: rand(0, W), w: rand(80, 210), tilt: rand(-0.35, 0.35),
|
| 1005 |
+
speed: rand(0.06, 0.14) * (Math.random() < 0.5 ? 1 : -1), alpha: rand(0.04, 0.09), blur,
|
| 1006 |
// undulation β two traveling waves so each beam shimmers like light refracted through moving water
|
| 1007 |
uAmp: rand(18, 46), uLen: rand(2.2, 4.5), uSpd: rand(0.35, 0.9) * (Math.random() < 0.5 ? 1 : -1), uPh: rand(0, Math.PI * 2),
|
| 1008 |
vAmp: rand(6, 14), vLen: rand(5, 9), vSpd: rand(0.8, 1.6) * (Math.random() < 0.5 ? 1 : -1), vPh: rand(0, Math.PI * 2)
|
|
|
|
| 1112 |
const [rr, rg, rb] = light.ray;
|
| 1113 |
const segs = RAY_SEGS;
|
| 1114 |
for (const r of rays) {
|
| 1115 |
+
// apply per-ray blur state: 0px (as-is), 3px (semi-blurry), or 7px (blurry)
|
| 1116 |
+
ctx.filter = r.blur > 0 ? `blur(${r.blur}px)` : 'none';
|
| 1117 |
// slow horizontal drift of the whole beam (kept from v1)
|
| 1118 |
const off = Math.sin(t * r.speed * 6 + r.x) * 55;
|
| 1119 |
const x0 = r.x + off;
|
|
|
|
| 1165 |
RC[i][0] = cxm + coreHalf; RC[i][1] = ry2;
|
| 1166 |
}
|
| 1167 |
band(LC, RC, r.alpha * 1.5, 0.65, 0);
|
| 1168 |
+
ctx.filter = 'none'; // reset blur before next ray or other drawing
|
| 1169 |
}
|
| 1170 |
}
|
| 1171 |
|
|
|
|
| 1274 |
// normal sway + a fast extra flinch while being pushed
|
| 1275 |
const sway = Math.sin(t * w.speed + w.phase + f * 2.6) * f * f * 24
|
| 1276 |
+ Math.sin(t * 7 + w.phase) * w.touch * f * 16;
|
| 1277 |
+
// v17: secondary wobble - a finer inner movement for unique per-stalk motion
|
| 1278 |
+
const sWobble = Math.sin(t * w.sFreq + w.sPh + f * 4.2) * w.sAmp * f;
|
| 1279 |
+
const px = w.x + (w.off[i] || 0) + stalkCurveX(w, f) + sway + sWobble;
|
| 1280 |
const py = baseY - w.h * f;
|
| 1281 |
w.points.push({ x: px, y: py });
|
| 1282 |
plantPoints.push({ x: px, y: py, r: w.width * 0.55, s: w });
|
|
|
|
| 1421 |
const curveX = f * g.cA1 * Math.sin(f * g.cF1 + g.cP1);
|
| 1422 |
// constant sway, amplified while rippling from a passing fish
|
| 1423 |
const sway = Math.sin(t * g.speed + g.phase + f * 2.4) * amp * f;
|
| 1424 |
+
// v17: secondary wobble - finer inner movement for unique per-blade motion
|
| 1425 |
+
const sWobble = Math.sin(t * g.sFreq + g.sPh + f * 3.8) * g.sAmp * f;
|
| 1426 |
pts.push({
|
| 1427 |
+
x: g.x + g.lean * g.h * f + curveX + sway + sWobble,
|
| 1428 |
y: g.y - g.h * f * (1 - g.ripple * 0.06)
|
| 1429 |
});
|
| 1430 |
}
|
|
|
|
| 1481 |
const curveX = f * g.cA1 * Math.sin(f * g.cF1 + g.cP1);
|
| 1482 |
// constant sway, weighted toward the tip
|
| 1483 |
const sway = Math.sin(t * g.speed + g.phase + f * 2.4) * amp * f;
|
| 1484 |
+
// v17: secondary wobble - finer inner movement for unique per-blade motion
|
| 1485 |
+
const sWobble = Math.sin(t * g.sFreq + g.sPh + f * 4.5) * g.sAmp * f;
|
| 1486 |
pts.push({
|
| 1487 |
+
x: g.x + g.lean * g.h * f + curveX + sway + sWobble,
|
| 1488 |
y: g.y - g.h * f
|
| 1489 |
});
|
| 1490 |
}
|