One commited on
Commit
661630c
Β·
verified Β·
1 Parent(s): 6a9924e

Upload index.html

Browse files
Files changed (1) hide show
  1. index.html +132 -22
index.html CHANGED
@@ -45,6 +45,39 @@ function lerpAngle(a, b, t) {
45
 
46
  const HUES = [8, 35, 46, 172, 195, 215, 262, 320];
47
  const TAIL_TYPES = ['classic', 'fan', 'streamer']; // v11: three tail-fin styles, one per fish
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  let fishes = [], bubbles = [], seaweeds = [], motes = [], rays = [], rocks = [], grass = [], fgGrass = [];
49
  let curNight = 0; // v7: shared night factor (dims the top-light + sheen)
50
  let plantPoints = []; // collision points along every stalk/branch, rebuilt each frame
@@ -57,7 +90,30 @@ class Fish {
57
  this.x = rand(60, W - 60);
58
  this.y = rand(H * 0.15, H * 0.72);
59
  this.angle = Math.random() < 0.5 ? rand(-0.4, 0.4) : Math.PI + rand(-0.4, 0.4);
60
- this.hue = pick(HUES);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  this.tailType = pick(TAIL_TYPES); // v11: random tail-fin style for variety
62
 
63
  // v13/v14/v15: random body markings β€” one roll per fish decides the look:
@@ -72,7 +128,11 @@ class Fish {
72
  const th = rand(0, Math.PI * 2);
73
  const rr = Math.sqrt(Math.random()) * 0.9; // uniform in ellipse, kept off the rim
74
  let style;
75
- if (pale) {
 
 
 
 
76
  // cream/ivory spots β€” pearl-gourami / leopard pattern
77
  style = `hsla(${this.hue + rand(-24, 24)},${rand(45, 70)}%,${rand(82, 94)}%,${rand(0.35, 0.6).toFixed(2)})`;
78
  } else {
@@ -94,11 +154,15 @@ class Fish {
94
  // jitter; each bar bends and tapers instead of being a straight line.
95
  const arr = [];
96
  const nStr = 5 + ((Math.random() * 4) | 0); // 5..8 bars
97
- const darkBars = Math.random() < 0.72; // mostly classic dark bars, sometimes pale
98
  for (let i = 0; i < nStr; i++) {
99
  const t = nStr > 1 ? i / (nStr - 1) : 0; // 0 behind head β†’ 1 near tail base
100
  let style;
101
- if (darkBars) {
 
 
 
 
102
  // classic dark tiger bars
103
  style = `hsla(${this.hue + rand(-14, 14)},${rand(65, 90)}%,${rand(18, 30)}%,${rand(0.5, 0.75).toFixed(2)})`;
104
  } else {
@@ -120,7 +184,8 @@ class Fish {
120
  }
121
  return arr;
122
  };
123
- const markRoll = Math.random();
 
124
  if (markRoll < 0.2) {
125
  this.spots = genSpots();
126
  this.stripes = null;
@@ -144,8 +209,8 @@ class Fish {
144
  this.whiskerLen = tier < 1 / 3 ? rand(0.45, 0.6)
145
  : tier < 2 / 3 ? rand(0.8, 1.0)
146
  : rand(1.25, 1.5); // in units of size s
147
- this.whiskPh = rand(0, Math.PI * 2); // per-fish sway phase
148
- this.whiskerAlpha = rand(0.45, 0.62); // delicate translucency
149
  } else {
150
  this.whiskerLen = null;
151
  }
@@ -254,7 +319,10 @@ class Fish {
254
  this.seaweedPref = (Math.random() * seaweeds.length) | 0;
255
  }
256
  const w = seaweeds[this.seaweedPref];
257
- const k = night * 0.8;
 
 
 
258
  wx += (w.x - wx) * k;
259
  wy += (w.yMid - wy) * k;
260
  }
@@ -270,6 +338,12 @@ class Fish {
270
  else if (this.x > W - mX) pull = Math.max(pull, 1 - (W - this.x) / mX);
271
  if (this.y < mTop) pull = Math.max(pull, 1 - this.y / mTop);
272
  else if (this.y > H - mBot) pull = Math.max(pull, 1 - (H - this.y) / mBot);
 
 
 
 
 
 
273
  if (pull > 0) {
274
  const cx = W / 2, cy = H * 0.45;
275
  desired = lerpAngle(desired, Math.atan2(cy - this.y, cx - this.x), Math.min(1, pull * 1.5));
@@ -384,7 +458,8 @@ class Fish {
384
  // tail fin (v7 rounded / v11 three styles): each tip is swept around by a curve
385
  // whose control point sits just past where the old spike was. The style is fixed
386
  // per fish at spawn so the school has variety in its tails.
387
- c.fillStyle = `hsla(${this.hue},75%,52%,0.92)`;
 
388
  c.beginPath();
389
  if (this.tailType === 'fan') {
390
  // Broad, rounded fan (betta/goldfish-style): wide spread, tips high and far out,
@@ -412,8 +487,9 @@ class Fish {
412
 
413
  // body β€” undulates while swimming: the ellipse flexes along a traveling wave (v3)
414
  const g = c.createLinearGradient(0, -s * 0.5, 0, s * 0.5);
415
- g.addColorStop(0, `hsl(${this.hue},82%,64%)`);
416
- g.addColorStop(1, `hsl(${this.hue},70%,44%)`);
 
417
  const NSEG = 26; // outline samples for a smooth curve
418
  // wave offset at any x along the body: rigid at head, full flex at tail.
419
  // Shared with the dorsal fin so it stays glued to the moving back (v4)
@@ -471,6 +547,22 @@ class Fish {
471
  c.restore();
472
  }
473
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
474
  // v13: random spots on ~20% of fish (or over the stripes for the 10% mixed) β€”
475
  // clipped to the body so they never spill, riding the traveling wave (wy) so they
476
  // flex with the undulating body, and drawn BEFORE the additive sheen so the
@@ -510,7 +602,9 @@ class Fish {
510
 
511
  // dorsal fin β€” rides the body wave at its attachment points so it
512
  // never disconnects as the back undulates (v4)
513
- c.fillStyle = `hsla(${this.hue},80%,50%,0.9)`;
 
 
514
  c.beginPath();
515
  c.moveTo(-s * 0.15, -s * 0.36 + wy(-s * 0.15));
516
  c.quadraticCurveTo(s * 0.05, -s * 0.78 + wy(s * 0.05), s * 0.38, -s * 0.4 + wy(s * 0.38));
@@ -522,10 +616,15 @@ class Fish {
522
  c.save();
523
  c.translate(s * 0.05, s * 0.12);
524
  c.rotate(0.7 + flap);
525
- c.fillStyle = `hsla(${this.hue},85%,66%,0.8)`;
 
 
526
  c.beginPath();
527
  c.ellipse(0, 0, s * 0.3, s * 0.14, 0, 0, Math.PI * 2);
528
  c.fill();
 
 
 
529
  c.restore();
530
 
531
  // eye
@@ -538,12 +637,16 @@ class Fish {
538
  c.arc(s * 0.61, -s * 0.1, s * 0.055, 0, Math.PI * 2);
539
  c.fill();
540
 
541
- // v16: whiskers (barbels) β€” a forward-curving pair from the snout on ~10% of fish,
542
- // in one of three lengths. Drawn in fish-local space so they track the heading, and
543
- // swaying gently with an independent phase so they read as soft feelers, not spikes.
 
 
 
544
  if (this.whiskerLen) {
545
  const L = this.whiskerLen * s; // chosen length tier at spawn
546
- const swayW = Math.sin(this.phase * 1.2 + this.whiskPh); // gentle independent sway
 
547
  c.save();
548
  c.lineCap = "round";
549
  c.strokeStyle = `hsla(${this.hue},55%,82%,${this.whiskerAlpha})`;
@@ -551,13 +654,20 @@ class Fish {
551
  for (const side of [-1, 1]) { // one whisker each side
552
  const rootX = s * 0.6;
553
  const rootY = side * s * 0.09; // just off the midline at the snout
554
- const tipX = rootX + L * (0.85 + 0.12 * Math.sin(this.whiskPh));
555
- const tipY = rootY + side * L * 0.36 + swayW * s * 0.1;
556
- const ctrlX = rootX + L * 0.55;
557
- const ctrlY = rootY - side * L * 0.1 - swayW * s * 0.07;
558
  c.beginPath();
559
  c.moveTo(rootX, rootY);
560
- c.quadraticCurveTo(ctrlX, ctrlY, tipX, tipY);
 
 
 
 
 
 
 
 
 
 
 
561
  c.stroke();
562
  }
563
  c.restore();
 
45
 
46
  const HUES = [8, 35, 46, 172, 195, 215, 262, 320];
47
  const TAIL_TYPES = ['classic', 'fan', 'streamer']; // v11: three tail-fin styles, one per fish
48
+ // v18: koi colourations β€” large irregular patches on a white/ivory base. Three classic
49
+ // looks: kohaku (red/orange), sanke (red + black), golden (amber/gold). Patches are
50
+ // sampled in units of `size` like the spots, so they scale with each fish.
51
+ function genKoiPatches(type) {
52
+ const palettes = {
53
+ kohaku: [
54
+ () => `hsla(${rand(4, 20)},${rand(85, 98)}%,${rand(50, 62)}%,0.92)`, // vermilion
55
+ () => `hsla(${rand(16, 32)},${rand(85, 98)}%,${rand(54, 66)}%,0.88)`, // orange
56
+ ],
57
+ sanke: [
58
+ () => `hsla(${rand(2, 14)},${rand(85, 98)}%,${rand(48, 60)}%,0.92)`, // red
59
+ () => `hsla(${rand(205, 230)},${rand(18, 38)}%,${rand(12, 22)}%,0.9)`, // charcoal/black
60
+ ],
61
+ golden: [
62
+ () => `hsla(${rand(38, 48)},${rand(85, 98)}%,${rand(55, 66)}%,0.92)`, // gold
63
+ () => `hsla(${rand(26, 40)},${rand(85, 98)}%,${rand(50, 62)}%,0.88)`, // amber
64
+ ],
65
+ };
66
+ const styles = palettes[type];
67
+ const arr = [];
68
+ const nPatches = 4 + ((Math.random() * 4) | 0); // 4..7 large blobs
69
+ for (let i = 0; i < nPatches; i++) {
70
+ const th = rand(0, Math.PI * 2);
71
+ const rr = Math.sqrt(Math.random()) * 0.85; // uniform in ellipse, kept off the rim
72
+ arr.push({
73
+ x: Math.cos(th) * rr, // along body axis (units of s)
74
+ y: Math.sin(th) * rr * 0.42, // across body (matches the 0.42 ellipse ratio)
75
+ r: rand(0.3, 0.52), // large patches (units of s)
76
+ style: styles[Math.random() < 0.5 ? 0 : 1](),
77
+ });
78
+ }
79
+ return arr;
80
+ }
81
  let fishes = [], bubbles = [], seaweeds = [], motes = [], rays = [], rocks = [], grass = [], fgGrass = [];
82
  let curNight = 0; // v7: shared night factor (dims the top-light + sheen)
83
  let plantPoints = []; // collision points along every stalk/branch, rebuilt each frame
 
90
  this.x = rand(60, W - 60);
91
  this.y = rand(H * 0.15, H * 0.72);
92
  this.angle = Math.random() < 0.5 ? rand(-0.4, 0.4) : Math.PI + rand(-0.4, 0.4);
93
+ // v18: color palette β€” silver/gray + three koi colourations (replaces dark blue & lime green)
94
+ const specialRoll = Math.random();
95
+ this.isKoi = false;
96
+ this.koiPatches = null;
97
+ if (specialRoll < 0.15) {
98
+ // Silver/gray fish β€” low saturation, medium-high lightness
99
+ this.hue = rand(200, 240); // slight blue-gray tint
100
+ this.saturation = rand(8, 18);
101
+ this.lightness = rand(60, 75);
102
+ } else if (specialRoll < 0.45) {
103
+ // Koi β€” a white/ivory base with large coloured patches; the three classic
104
+ // colourations are split roughly evenly across this 30% band.
105
+ this.isKoi = true;
106
+ const koiType = specialRoll < 0.25 ? 'kohaku' : specialRoll < 0.35 ? 'sanke' : 'golden';
107
+ this.hue = rand(40, 50); // warm ivory tint for the base
108
+ this.saturation = rand(12, 22); // low saturation β†’ white body
109
+ this.lightness = rand(88, 94); // high lightness β†’ white/ivory base
110
+ this.koiPatches = genKoiPatches(koiType);
111
+ } else {
112
+ // Standard colored fish from the original palette
113
+ this.hue = pick(HUES);
114
+ this.saturation = 75;
115
+ this.lightness = 52;
116
+ }
117
  this.tailType = pick(TAIL_TYPES); // v11: random tail-fin style for variety
118
 
119
  // v13/v14/v15: random body markings β€” one roll per fish decides the look:
 
128
  const th = rand(0, Math.PI * 2);
129
  const rr = Math.sqrt(Math.random()) * 0.9; // uniform in ellipse, kept off the rim
130
  let style;
131
+ if (this.saturation < 30) {
132
+ // v14: gray/silver fish β€” keep spots low-saturation to match
133
+ const spotLight = pale ? rand(75, 88) : rand(35, 50);
134
+ style = `hsla(${this.hue + rand(-24, 24)},${rand(5, 15)}%,${spotLight}%,${rand(0.35, 0.6).toFixed(2)})`;
135
+ } else if (pale) {
136
  // cream/ivory spots β€” pearl-gourami / leopard pattern
137
  style = `hsla(${this.hue + rand(-24, 24)},${rand(45, 70)}%,${rand(82, 94)}%,${rand(0.35, 0.6).toFixed(2)})`;
138
  } else {
 
154
  // jitter; each bar bends and tapers instead of being a straight line.
155
  const arr = [];
156
  const nStr = 5 + ((Math.random() * 4) | 0); // 5..8 bars
157
+ const darkBars = this.saturation >= 30 && Math.random() < 0.72; // v14: gray fish get light stripes
158
  for (let i = 0; i < nStr; i++) {
159
  const t = nStr > 1 ? i / (nStr - 1) : 0; // 0 behind head β†’ 1 near tail base
160
  let style;
161
+ if (this.saturation < 30) {
162
+ // v14: gray/silver fish β€” keep stripes low-saturation to match
163
+ const stripeLight = darkBars ? rand(35, 50) : rand(75, 88);
164
+ style = `hsla(${this.hue + rand(-14, 14)},${rand(5, 15)}%,${stripeLight}%,${rand(0.5, 0.75).toFixed(2)})`;
165
+ } else if (darkBars) {
166
  // classic dark tiger bars
167
  style = `hsla(${this.hue + rand(-14, 14)},${rand(65, 90)}%,${rand(18, 30)}%,${rand(0.5, 0.75).toFixed(2)})`;
168
  } else {
 
184
  }
185
  return arr;
186
  };
187
+ // v18: koi already carry their own large patches β€” skip the random spots/stripes
188
+ const markRoll = this.isKoi ? 1 : Math.random();
189
  if (markRoll < 0.2) {
190
  this.spots = genSpots();
191
  this.stripes = null;
 
209
  this.whiskerLen = tier < 1 / 3 ? rand(0.45, 0.6)
210
  : tier < 2 / 3 ? rand(0.8, 1.0)
211
  : rand(1.25, 1.5); // in units of size s
212
+ this.whiskPh = rand(0, Math.PI * 2); // per-fish wave phase offset
213
+ this.whiskerAlpha = 0.5; // v18: fixed 50% opacity
214
  } else {
215
  this.whiskerLen = null;
216
  }
 
319
  this.seaweedPref = (Math.random() * seaweeds.length) | 0;
320
  }
321
  const w = seaweeds[this.seaweedPref];
322
+ // v13: plinko fix β€” stalks near the sides pull harder so fish actually reach them
323
+ // instead of being deflected back toward the middle by the boundary steering.
324
+ const edgeT = Math.abs(w.x / W - 0.5) * 2; // 0 at center β†’ 1 at glass
325
+ const k = night * 0.8 * (1 + edgeT * 0.9); // up to ~1.9x at the very edge
326
  wx += (w.x - wx) * k;
327
  wy += (w.yMid - wy) * k;
328
  }
 
338
  else if (this.x > W - mX) pull = Math.max(pull, 1 - (W - this.x) / mX);
339
  if (this.y < mTop) pull = Math.max(pull, 1 - this.y / mTop);
340
  else if (this.y > H - mBot) pull = Math.max(pull, 1 - (H - this.y) / mBot);
341
+ // v13: plinko fix β€” grass-sleepers settling at night are less deflected by the
342
+ // boundary, so they can reach side positions instead of being pushed to the middle.
343
+ if (pull > 0 && this.grassSleeper && night > 0.25) {
344
+ const settle = Math.min(1, (night - 0.25) / 0.75); // ramps up as they get sleepier
345
+ pull *= (1 - settle * 0.55); // up to ~55% weaker when fully settled
346
+ }
347
  if (pull > 0) {
348
  const cx = W / 2, cy = H * 0.45;
349
  desired = lerpAngle(desired, Math.atan2(cy - this.y, cx - this.x), Math.min(1, pull * 1.5));
 
458
  // tail fin (v7 rounded / v11 three styles): each tip is swept around by a curve
459
  // whose control point sits just past where the old spike was. The style is fixed
460
  // per fish at spawn so the school has variety in its tails.
461
+ // v14: use per-fish saturation and lightness for expanded color palette
462
+ c.fillStyle = `hsla(${this.hue},${this.saturation}%,${this.lightness}%,0.92)`;
463
  c.beginPath();
464
  if (this.tailType === 'fan') {
465
  // Broad, rounded fan (betta/goldfish-style): wide spread, tips high and far out,
 
487
 
488
  // body β€” undulates while swimming: the ellipse flexes along a traveling wave (v3)
489
  const g = c.createLinearGradient(0, -s * 0.5, 0, s * 0.5);
490
+ // v14: use per-fish saturation and lightness for expanded color palette
491
+ g.addColorStop(0, `hsl(${this.hue},${Math.min(100, this.saturation * 1.08)}%,${this.lightness + 12}%)`);
492
+ g.addColorStop(1, `hsl(${this.hue},${this.saturation * 0.9}%,${this.lightness - 8}%)`);
493
  const NSEG = 26; // outline samples for a smooth curve
494
  // wave offset at any x along the body: rigid at head, full flex at tail.
495
  // Shared with the dorsal fin so it stays glued to the moving back (v4)
 
547
  c.restore();
548
  }
549
 
550
+ // v18: koi patches β€” large irregular blobs clipped to the body outline, riding the
551
+ // traveling wave (wy) so they flex with the undulating body, drawn BEFORE the
552
+ // additive sheen so the iridescent shimmer still plays across them.
553
+ if (this.koiPatches) {
554
+ c.save();
555
+ c.clip(bodyPath);
556
+ for (const kp of this.koiPatches) {
557
+ const sx = kp.x * s;
558
+ c.fillStyle = kp.style;
559
+ c.beginPath();
560
+ c.arc(sx, kp.y * s + wy(sx), kp.r * s, 0, Math.PI * 2);
561
+ c.fill();
562
+ }
563
+ c.restore();
564
+ }
565
+
566
  // v13: random spots on ~20% of fish (or over the stripes for the 10% mixed) β€”
567
  // clipped to the body so they never spill, riding the traveling wave (wy) so they
568
  // flex with the undulating body, and drawn BEFORE the additive sheen so the
 
602
 
603
  // dorsal fin β€” rides the body wave at its attachment points so it
604
  // never disconnects as the back undulates (v4)
605
+ // v17: matches the fish's own color instead of a fixed blue β€” silver fish no longer
606
+ // sprout mismatched blue fins
607
+ c.fillStyle = `hsla(${this.hue},${this.saturation}%,${this.lightness}%,0.9)`;
608
  c.beginPath();
609
  c.moveTo(-s * 0.15, -s * 0.36 + wy(-s * 0.15));
610
  c.quadraticCurveTo(s * 0.05, -s * 0.78 + wy(s * 0.05), s * 0.38, -s * 0.4 + wy(s * 0.38));
 
616
  c.save();
617
  c.translate(s * 0.05, s * 0.12);
618
  c.rotate(0.7 + flap);
619
+ // v17: pectoral fin matches the body color (no more blue arms on silver fish), but
620
+ // gets a thin darker outline so it stays visible sitting on top of the same-colored body
621
+ c.fillStyle = `hsla(${this.hue},${this.saturation}%,${this.lightness}%,0.8)`;
622
  c.beginPath();
623
  c.ellipse(0, 0, s * 0.3, s * 0.14, 0, 0, Math.PI * 2);
624
  c.fill();
625
+ c.strokeStyle = `hsla(${this.hue},${Math.min(100, this.saturation * 1.15)}%,${Math.max(8, this.lightness - 20)}%,0.5)`;
626
+ c.lineWidth = Math.max(1, s * 0.03);
627
+ c.stroke();
628
  c.restore();
629
 
630
  // eye
 
637
  c.arc(s * 0.61, -s * 0.1, s * 0.055, 0, Math.PI * 2);
638
  c.fill();
639
 
640
+ // v16/v17: whiskers (barbels) β€” a pair from the snout on ~10% of fish, in one of
641
+ // three lengths, drawn in fish-local space so they track the heading.
642
+ // v18: loose ribbons flowing in the current β€” they trail back opposite the motion
643
+ // and a traveling wave runs down their length as the fish swims. The faster it
644
+ // swims, the further back they stream and the livelier they ripple; at rest they
645
+ // droop softly. Drawn at a fixed 50% opacity.
646
  if (this.whiskerLen) {
647
  const L = this.whiskerLen * s; // chosen length tier at spawn
648
+ const spd = Math.min(1, this.speedFactor); // current speed multiplier
649
+ const NPTS = 7; // sample points per whisker
650
  c.save();
651
  c.lineCap = "round";
652
  c.strokeStyle = `hsla(${this.hue},55%,82%,${this.whiskerAlpha})`;
 
654
  for (const side of [-1, 1]) { // one whisker each side
655
  const rootX = s * 0.6;
656
  const rootY = side * s * 0.09; // just off the midline at the snout
 
 
 
 
657
  c.beginPath();
658
  c.moveTo(rootX, rootY);
659
+ for (let i = 1; i <= NPTS; i++) {
660
+ const t = i / NPTS; // 0 at root β†’ 1 at tip
661
+ // base flow: streams back with the current as the fish moves forward,
662
+ // drooping gently outward, easing out toward the loose tip
663
+ const bx = rootX - L * (0.25 + 0.65 * spd) * t;
664
+ const by = rootY + side * L * 0.42 * t * t;
665
+ // traveling wave: ripples flow down the whisker (root β†’ tip), loosest at the
666
+ // free end; this.phase advances with swim speed so they live up as it moves
667
+ const amp = s * 0.17 * t * (0.35 + 0.65 * spd);
668
+ const off = Math.sin(t * 5.5 - this.phase * 1.4 + this.whiskPh) * amp;
669
+ c.lineTo(bx, by + off);
670
+ }
671
  c.stroke();
672
  }
673
  c.restore();