Upload index.html
Browse files- index.html +59 -21
index.html
CHANGED
|
@@ -514,6 +514,36 @@ class Fish {
|
|
| 514 |
}
|
| 515 |
c.fill();
|
| 516 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 517 |
// body — undulates while swimming: the ellipse flexes along a traveling wave (v3)
|
| 518 |
const g = this.bodyGrad; // precomputed in constructor (static colors, local coords)
|
| 519 |
const NSEG = 26; // outline samples for a smooth curve
|
|
@@ -655,33 +685,29 @@ class Fish {
|
|
| 655 |
c.arc(s * 0.61, -s * 0.1, s * 0.055, 0, Math.PI * 2);
|
| 656 |
c.fill();
|
| 657 |
|
| 658 |
-
//
|
| 659 |
-
//
|
| 660 |
-
//
|
| 661 |
-
// and a traveling wave
|
| 662 |
-
//
|
| 663 |
-
// droop softly. Drawn at a fixed 50% opacity.
|
| 664 |
if (this.whiskerLen) {
|
| 665 |
-
const L = this.whiskerLen * s;
|
| 666 |
-
const spd = Math.min(1, this.speedFactor);
|
| 667 |
-
const NPTS = 7;
|
| 668 |
c.save();
|
| 669 |
c.lineCap = "round";
|
| 670 |
c.strokeStyle = this.whiskerStyle;
|
| 671 |
c.lineWidth = Math.max(1, s * 0.045);
|
| 672 |
-
|
|
|
|
| 673 |
const rootX = s * 0.6;
|
| 674 |
-
const rootY = side * s * 0.09;
|
| 675 |
c.beginPath();
|
| 676 |
c.moveTo(rootX, rootY);
|
| 677 |
for (let i = 1; i <= NPTS; i++) {
|
| 678 |
-
const t = i / NPTS;
|
| 679 |
-
// base flow: streams back with the current as the fish moves forward,
|
| 680 |
-
// drooping gently outward, easing out toward the loose tip
|
| 681 |
const bx = rootX - L * (0.25 + 0.65 * spd) * t;
|
| 682 |
const by = rootY + side * L * 0.42 * t * t;
|
| 683 |
-
// traveling wave: ripples flow down the whisker (root → tip), loosest at the
|
| 684 |
-
// free end; this.phase advances with swim speed so they live up as it moves
|
| 685 |
const amp = s * 0.17 * t * (0.35 + 0.65 * spd);
|
| 686 |
const off = Math.sin(t * 5.5 - this.phase * 1.4 + this.whiskPh) * amp;
|
| 687 |
c.lineTo(bx, by + off);
|
|
@@ -772,6 +798,15 @@ function strokeArc(c, pts) {
|
|
| 772 |
c.stroke();
|
| 773 |
}
|
| 774 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 775 |
class Bubble {
|
| 776 |
constructor(init, x, y, size) {
|
| 777 |
if (x !== undefined) { // spawned from a fish's mouth: tiny, one-shot
|
|
@@ -780,6 +815,7 @@ class Bubble {
|
|
| 780 |
this.r = rand(1.4, 3.2) * s;
|
| 781 |
this.speed = rand(45, 85) * (0.7 + 0.3 * s);
|
| 782 |
this.p = rand(0, Math.PI * 2);
|
|
|
|
| 783 |
this.isMouth = true;
|
| 784 |
} else {
|
| 785 |
this.reset(init);
|
|
@@ -791,6 +827,7 @@ class Bubble {
|
|
| 791 |
this.r = rand(2, 7);
|
| 792 |
this.speed = rand(35, 90);
|
| 793 |
this.p = rand(0, Math.PI * 2);
|
|
|
|
| 794 |
this.isMouth = false;
|
| 795 |
}
|
| 796 |
update(dt, t) {
|
|
@@ -799,14 +836,15 @@ class Bubble {
|
|
| 799 |
if (this.y < -12) { if (this.isMouth) this.dead = true; else this.reset(false); }
|
| 800 |
}
|
| 801 |
draw(c) {
|
|
|
|
| 802 |
c.lineWidth = 1;
|
| 803 |
-
c.strokeStyle =
|
| 804 |
-
c.fillStyle =
|
| 805 |
c.beginPath();
|
| 806 |
c.arc(this.x, this.y, this.r, 0, Math.PI * 2);
|
| 807 |
c.fill();
|
| 808 |
c.stroke();
|
| 809 |
-
c.fillStyle =
|
| 810 |
c.beginPath();
|
| 811 |
c.arc(this.x - this.r * 0.3, this.y - this.r * 0.3, this.r * 0.25, 0, Math.PI * 2);
|
| 812 |
c.fill();
|
|
@@ -1000,9 +1038,9 @@ function buildScene() {
|
|
| 1000 |
}
|
| 1001 |
|
| 1002 |
for (let i = 0; i < 5; i++) {
|
| 1003 |
-
// random blur state:
|
| 1004 |
const blurRoll = Math.random();
|
| 1005 |
-
const blur = blurRoll < 0.34 ?
|
| 1006 |
rays.push({
|
| 1007 |
x: rand(0, W), w: rand(80, 210), tilt: rand(-0.35, 0.35),
|
| 1008 |
speed: rand(0.06, 0.14) * (Math.random() < 0.5 ? 1 : -1), alpha: rand(0.04, 0.09), blur,
|
|
|
|
| 514 |
}
|
| 515 |
c.fill();
|
| 516 |
|
| 517 |
+
// v21: far-side whisker — drawn BEFORE the body so it sits behind the fish and is
|
| 518 |
+
// partially obscured by the head (only the part that pokes out from behind the snout
|
| 519 |
+
// shows). The near-side whisker is drawn after the eye, below.
|
| 520 |
+
if (this.whiskerLen) {
|
| 521 |
+
const L = this.whiskerLen * s;
|
| 522 |
+
const spd = Math.min(1, this.speedFactor);
|
| 523 |
+
const NPTS = 7;
|
| 524 |
+
c.save();
|
| 525 |
+
c.lineCap = "round";
|
| 526 |
+
c.strokeStyle = this.whiskerStyle;
|
| 527 |
+
c.lineWidth = Math.max(1, s * 0.045);
|
| 528 |
+
{
|
| 529 |
+
const side = -1; // far side (behind the body)
|
| 530 |
+
const rootX = s * 0.6;
|
| 531 |
+
const rootY = side * s * 0.09;
|
| 532 |
+
c.beginPath();
|
| 533 |
+
c.moveTo(rootX, rootY);
|
| 534 |
+
for (let i = 1; i <= NPTS; i++) {
|
| 535 |
+
const t = i / NPTS;
|
| 536 |
+
const bx = rootX - L * (0.25 + 0.65 * spd) * t;
|
| 537 |
+
const by = rootY + side * L * 0.42 * t * t;
|
| 538 |
+
const amp = s * 0.17 * t * (0.35 + 0.65 * spd);
|
| 539 |
+
const off = Math.sin(t * 5.5 - this.phase * 1.4 + this.whiskPh) * amp;
|
| 540 |
+
c.lineTo(bx, by + off);
|
| 541 |
+
}
|
| 542 |
+
c.stroke();
|
| 543 |
+
}
|
| 544 |
+
c.restore();
|
| 545 |
+
}
|
| 546 |
+
|
| 547 |
// body — undulates while swimming: the ellipse flexes along a traveling wave (v3)
|
| 548 |
const g = this.bodyGrad; // precomputed in constructor (static colors, local coords)
|
| 549 |
const NSEG = 26; // outline samples for a smooth curve
|
|
|
|
| 685 |
c.arc(s * 0.61, -s * 0.1, s * 0.055, 0, Math.PI * 2);
|
| 686 |
c.fill();
|
| 687 |
|
| 688 |
+
// v21: near-side whisker — drawn AFTER the body so it sits in front of the fish.
|
| 689 |
+
// (The far-side whisker is drawn before the body, above.) Together they form a
|
| 690 |
+
// pair from the snout on ~10% of fish, in one of three lengths, with loose ribbons
|
| 691 |
+
// flowing in the current and a traveling wave rippling down their length. Drawn at
|
| 692 |
+
// a fixed 50% opacity.
|
|
|
|
| 693 |
if (this.whiskerLen) {
|
| 694 |
+
const L = this.whiskerLen * s;
|
| 695 |
+
const spd = Math.min(1, this.speedFactor);
|
| 696 |
+
const NPTS = 7;
|
| 697 |
c.save();
|
| 698 |
c.lineCap = "round";
|
| 699 |
c.strokeStyle = this.whiskerStyle;
|
| 700 |
c.lineWidth = Math.max(1, s * 0.045);
|
| 701 |
+
{
|
| 702 |
+
const side = 1; // near side (in front of the body)
|
| 703 |
const rootX = s * 0.6;
|
| 704 |
+
const rootY = side * s * 0.09;
|
| 705 |
c.beginPath();
|
| 706 |
c.moveTo(rootX, rootY);
|
| 707 |
for (let i = 1; i <= NPTS; i++) {
|
| 708 |
+
const t = i / NPTS;
|
|
|
|
|
|
|
| 709 |
const bx = rootX - L * (0.25 + 0.65 * spd) * t;
|
| 710 |
const by = rootY + side * L * 0.42 * t * t;
|
|
|
|
|
|
|
| 711 |
const amp = s * 0.17 * t * (0.35 + 0.65 * spd);
|
| 712 |
const off = Math.sin(t * 5.5 - this.phase * 1.4 + this.whiskPh) * amp;
|
| 713 |
c.lineTo(bx, by + off);
|
|
|
|
| 798 |
c.stroke();
|
| 799 |
}
|
| 800 |
|
| 801 |
+
// v19: each bubble gets a subtle random tint — pure white up to a soft grey-blue
|
| 802 |
+
function bubbleColor() {
|
| 803 |
+
const t = Math.random() * 0.85; // keep it subtle, most stay near-white
|
| 804 |
+
const r = Math.round(255 - (255 - 196) * t);
|
| 805 |
+
const g = Math.round(255 - (255 - 214) * t);
|
| 806 |
+
const b = Math.round(255 - (255 - 238) * t);
|
| 807 |
+
return [r, g, b];
|
| 808 |
+
}
|
| 809 |
+
|
| 810 |
class Bubble {
|
| 811 |
constructor(init, x, y, size) {
|
| 812 |
if (x !== undefined) { // spawned from a fish's mouth: tiny, one-shot
|
|
|
|
| 815 |
this.r = rand(1.4, 3.2) * s;
|
| 816 |
this.speed = rand(45, 85) * (0.7 + 0.3 * s);
|
| 817 |
this.p = rand(0, Math.PI * 2);
|
| 818 |
+
this.rgb = bubbleColor();
|
| 819 |
this.isMouth = true;
|
| 820 |
} else {
|
| 821 |
this.reset(init);
|
|
|
|
| 827 |
this.r = rand(2, 7);
|
| 828 |
this.speed = rand(35, 90);
|
| 829 |
this.p = rand(0, Math.PI * 2);
|
| 830 |
+
this.rgb = bubbleColor();
|
| 831 |
this.isMouth = false;
|
| 832 |
}
|
| 833 |
update(dt, t) {
|
|
|
|
| 836 |
if (this.y < -12) { if (this.isMouth) this.dead = true; else this.reset(false); }
|
| 837 |
}
|
| 838 |
draw(c) {
|
| 839 |
+
const [r, g, b] = this.rgb;
|
| 840 |
c.lineWidth = 1;
|
| 841 |
+
c.strokeStyle = `rgba(${r},${g},${b},0.35)`;
|
| 842 |
+
c.fillStyle = `rgba(${r},${g},${b},0.08)`;
|
| 843 |
c.beginPath();
|
| 844 |
c.arc(this.x, this.y, this.r, 0, Math.PI * 2);
|
| 845 |
c.fill();
|
| 846 |
c.stroke();
|
| 847 |
+
c.fillStyle = `rgba(${r},${g},${b},0.45)`;
|
| 848 |
c.beginPath();
|
| 849 |
c.arc(this.x - this.r * 0.3, this.y - this.r * 0.3, this.r * 0.25, 0, Math.PI * 2);
|
| 850 |
c.fill();
|
|
|
|
| 1038 |
}
|
| 1039 |
|
| 1040 |
for (let i = 0; i < 5; i++) {
|
| 1041 |
+
// random blur state: ~3px (semi-blurry), ~7px (blurry), or ~14px (very blurry) for visual variety
|
| 1042 |
const blurRoll = Math.random();
|
| 1043 |
+
const blur = blurRoll < 0.34 ? 3 : (blurRoll < 0.67 ? 7 : 14);
|
| 1044 |
rays.push({
|
| 1045 |
x: rand(0, W), w: rand(80, 210), tilt: rand(-0.35, 0.35),
|
| 1046 |
speed: rand(0.06, 0.14) * (Math.random() < 0.5 ? 1 : -1), alpha: rand(0.04, 0.09), blur,
|