One commited on
Commit
8b491b6
·
verified ·
1 Parent(s): ca9dc46

Upload index.html

Browse files
Files changed (1) hide show
  1. index.html +211 -125
index.html CHANGED
@@ -83,7 +83,7 @@ function genKoiPatches(type) {
83
  }
84
  return arr;
85
  }
86
- let fishes = [], bubbles = [], seaweeds = [], backSeaweeds = [], motes = [], rays = [], rocks = [], grass = [], fgGrass = [];
87
  let playerFish = null; // the fish controlled by gamepad (spawned with F key)
88
  const GAMEPAD_DEADZONE = 0.15; // ignore small stick movements
89
  let curNight = 0; // v7: shared night factor (dims the top-light + sheen)
@@ -894,6 +894,7 @@ function buildScene() {
894
  bubbles = [];
895
  seaweeds = [];
896
  backSeaweeds = []; // v4: blurry background seaweed (not fish-touchable)
 
897
  motes = [];
898
  rays = [];
899
  rocks = [];
@@ -1008,9 +1009,8 @@ function buildScene() {
1008
  const nStalks = 2 + ((Math.random() * 3) | 0); // 2..4 stalks per bundle
1009
  for (let j = 0; j < nStalks; j++) {
1010
  const hue = clusterHue + rand(-15, 15);
1011
- // v4.2: depth split — ~66% "near" (half blur, slightly thinner) for a layered look,
1012
- // the rest stay far back with the heavy blur.
1013
- const near = Math.random() < 0.66;
1014
  const st = {
1015
  x: cx + rand(-20, 20),
1016
  h: rand(H * 0.35, H * 0.65), // much taller than foreground seaweed
@@ -1019,16 +1019,16 @@ function buildScene() {
1019
  speed: rand(0.7, 1.3), // slower, more gentle sway
1020
  hue,
1021
  near,
1022
- blur: near ? 4 : 8, // baked into sprite at build time (no runtime cost)
1023
  width: near ? rand(2.5, 5) : rand(3, 6),
1024
  tipW: near ? rand(1.2, 2.4) : rand(1.5, 3),
1025
  branches: [],
1026
  // organic curve parameters (same as foreground seaweed)
1027
  cA1: rand(8, 15), cF1: rand(0.8, 1.6), cP1: rand(0, Math.PI * 2),
1028
  cA2: rand(3, 8), cF2: rand(2.0, 3.2), cP2: rand(0, Math.PI * 2),
1029
- // PERF: pre-rendered sprite with blur baked in (no runtime blur cost)
1030
- sprite: null,
1031
- spriteScale: 1
 
1032
  };
1033
  // v4.1: add branches to background seaweed (like foreground)
1034
  const nBr = 1 + ((Math.random() * 3) | 0); // 1..3 branches per stalk
@@ -1046,7 +1046,44 @@ function buildScene() {
1046
  backSeaweeds.push(st);
1047
  }
1048
  }
1049
- buildBackSeaweedSprites();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1050
 
1051
  // Short, dense grass-like ground cover along the sand bed.
1052
  // Each tuft is a cluster of thin blades that ripple when fish swim nearby.
@@ -1171,6 +1208,7 @@ function resize() {
1171
  canvas.height = H * dpr;
1172
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
1173
  buildScene();
 
1174
  }
1175
 
1176
  // --- slow light cycle: bright day -> dusk -> night -> dawn, over CYCLE seconds ---
@@ -1447,26 +1485,114 @@ function stalkCurveX(w, f) {
1447
  return f * (w.cA1 * Math.sin(f * w.cF1 + w.cP1) + w.cA2 * Math.sin(f * w.cF2 + w.cP2));
1448
  }
1449
 
1450
- // v4: pre-rendered blurry sprites for background seaweed (no runtime blur cost)
1451
- function buildBackSeaweedSprites() {
1452
- const BLUR_MAX = 8; // far-back blur; near stalks use half (see st.blur)
1453
- const PAD = 30; // extra space around stalk for blur spill
1454
-
 
1455
  for (const w of backSeaweeds) {
1456
- // Calculate bounding box
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1457
  let xMin = Infinity, xMax = -Infinity;
1458
 
1459
- // Sample the stalk curve to find bounds
1460
  for (let i = 0; i <= w.segs; i++) {
1461
  const f = i / w.segs;
1462
- const px = w.x + stalkCurveX(w, f);
1463
- if (px < xMin) xMin = px;
1464
- if (px > xMax) xMax = px;
1465
- }
1466
-
1467
- // Include branch reach in bounding box
1468
- for (const b of w.branches) {
1469
- const px = w.x + stalkCurveX(w, b.f) + b.side * b.len;
1470
  if (px < xMin) xMin = px;
1471
  if (px > xMax) xMax = px;
1472
  }
@@ -1484,125 +1610,83 @@ function buildBackSeaweedSprites() {
1484
  spr.height = bh;
1485
  const sc = spr.getContext("2d");
1486
 
1487
- // Apply per-stalk blur filter (baked in at build time — zero runtime cost)
1488
- const BLUR = w.blur || BLUR_MAX;
1489
  sc.filter = `blur(${BLUR}px)`;
1490
 
1491
- // Draw the stalk as a tapered blade
1492
- const points = [];
 
 
 
 
 
 
 
 
 
 
 
 
1493
  for (let i = 0; i <= w.segs; i++) {
1494
  const f = i / w.segs;
1495
- const px = w.x + stalkCurveX(w, f);
1496
- // Transform to sprite coordinates — root at BOTTOM of sprite, tip at top
1497
- points.push({
1498
- x: px - xMin,
1499
- y: PAD + w.h * (1 - f)
1500
- });
 
1501
  }
1502
 
1503
- // Draw tapered blade outline
1504
- sc.fillStyle = `hsla(${w.hue},50%,32%,0.85)`;
1505
- sc.beginPath();
1506
- for (let i = 0; i < points.length; i++) {
1507
- const p = points[i];
1508
- const a = points[Math.max(0, i - 1)];
1509
- const b = points[Math.min(points.length - 1, i + 1)];
1510
- let dx = b.x - a.x, dy = b.y - a.y;
1511
- const dl = Math.hypot(dx, dy) || 1;
1512
- const nx = -dy / dl, ny = dx / dl;
1513
- const f = i / (points.length - 1);
1514
- const hw = (w.width * (1 - f) + w.tipW * f) / 2;
1515
- if (i === 0) sc.moveTo(p.x + nx * hw, p.y + ny * hw);
1516
- else sc.lineTo(p.x + nx * hw, p.y + ny * hw);
1517
- }
1518
- // Rounded tip
1519
- const tp = points[points.length - 1], pp = points[points.length - 2];
1520
- let dx = tp.x - pp.x, dy = tp.y - pp.y;
1521
- const dl = Math.hypot(dx, dy) || 1;
1522
- const nx = -dy / dl, ny = dx / dl;
1523
- const hwT = w.tipW / 2;
1524
- sc.quadraticCurveTo(tp.x + dx, tp.y + dy * 0.6, tp.x - nx * hwT, tp.y - ny * hwT);
1525
- // Down the other side
1526
- for (let i = points.length - 1; i >= 0; i--) {
1527
- const p = points[i];
1528
- const a = points[Math.max(0, i - 1)];
1529
- const b = points[Math.min(points.length - 1, i + 1)];
1530
- let dx = b.x - a.x, dy = b.y - a.y;
1531
- const dl = Math.hypot(dx, dy) || 1;
1532
- const nx = -dy / dl, ny = dx / dl;
1533
- const f = i / (points.length - 1);
1534
- const hw = (w.width * (1 - f) + w.tipW * f) / 2;
1535
- sc.lineTo(p.x - nx * hw, p.y - ny * hw);
1536
- }
1537
- sc.closePath();
1538
- sc.fill();
1539
 
1540
- // v4.1: draw branches (same style as foreground seaweed)
1541
  for (const b of w.branches) {
1542
  const idx = Math.min(w.segs, Math.max(1, Math.round(b.f * w.segs)));
1543
- const a = points[idx];
1544
-
1545
- // Calculate branch endpoint in sprite coordinates
1546
- const bx = w.x + stalkCurveX(w, b.f) + b.side * b.len;
1547
- const by = PAD + w.h * (1 - b.f) - b.up;
1548
 
1549
- // Transform to sprite-local coordinates
1550
- const btx = bx - xMin;
1551
- const bty = by;
1552
-
1553
- // S-curved frond (same as foreground)
1554
- const c1x = a.x + b.side * b.len * 0.3;
1555
- const c1y = a.y - b.up * 0.75;
1556
- const c2x = btx - b.side * b.len * 0.35;
1557
- const c2y = bty + b.up * 0.6;
1558
-
1559
- sc.strokeStyle = `hsla(${w.hue + 8},55%,42%,0.8)`; // slightly lighter than stalk
1560
- sc.lineWidth = b.w;
1561
  sc.beginPath();
1562
- sc.moveTo(a.x, a.y);
1563
- sc.bezierCurveTo(c1x, c1y, c2x, c2y, btx, bty);
 
 
 
 
 
 
 
 
1564
  sc.stroke();
1565
  }
1566
 
1567
- // Store sprite and its anchor point
1568
- // BUG FIX: store RELATIVE offset from stalk base, not absolute screen position
1569
  w.sprite = spr;
1570
- w.spriteXOffset = xMin - w.x; // relative offset (was causing all seaweed to render at same x)
1571
- }
1572
- }
1573
-
1574
- // v4: update background seaweed wave motion (no fish interaction)
1575
- function updateBackSeaweeds(t) {
1576
- for (const w of backSeaweeds) {
1577
- // Simple sine-based sway (much simpler than foreground seaweed physics)
1578
- const baseY = sandAt(w.x) + 10;
1579
-
1580
- // Calculate the current sway offset based on time
1581
- const swayPhase = t * w.speed + w.phase;
1582
-
1583
- // Store the current wave state for drawing
1584
- w.currentSway = Math.sin(swayPhase);
1585
- w.currentSway2 = Math.sin(swayPhase * 1.3 + 0.5); // secondary wave for organic motion
1586
  }
1587
  }
1588
 
1589
- // v4: draw background seaweed (blurred sprites, behind everything else)
1590
- function drawBackSeaweeds() {
1591
- for (const w of backSeaweeds) {
1592
  if (!w.sprite) continue;
1593
-
1594
  const baseY = sandAt(w.x) + 10;
1595
-
1596
- // Gentle sway: small rotation about the planted base + a tiny root shift,
1597
- // so it reads as bending in the current instead of sliding sideways.
1598
- const swayA = w.currentSway * 0.035 + w.currentSway2 * 0.018;
1599
- const swayX = w.currentSway * 3;
1600
-
1601
  ctx.save();
1602
- ctx.translate(w.x + swayX, baseY);
1603
- ctx.rotate(swayA);
1604
- // Sprite root sits at sprite-y (h+30); drawing at -h-30 lands it exactly on baseY
1605
- ctx.drawImage(w.sprite, -w.spriteXOffset, -w.h - 30);
 
 
1606
  ctx.restore();
1607
  }
1608
  }
@@ -2004,9 +2088,11 @@ function frame(now) {
2004
  drawRays(t, light);
2005
  updateMotes(dt, t);
2006
 
 
 
 
2007
  // v4: background seaweed — drawn behind sand/rocks for depth
2008
- updateBackSeaweeds(t);
2009
- drawBackSeaweeds();
2010
 
2011
  updatePlants(t, dt);
2012
  updateGrass(t, dt);
@@ -2076,7 +2162,7 @@ function frame(now) {
2076
  function doResize() {
2077
  resize();
2078
  buildRaySprites(); // v23: rebuild the pre-blurred beam sprites for the new size
2079
- buildBackSeaweedSprites(); // v4: rebuild background seaweed sprites for new size
2080
  buildStaticLayer();
2081
  if (!grainPattern) buildGrainTile(); // v8: grain tile is size-independent, build once
2082
  }
 
83
  }
84
  return arr;
85
  }
86
+ let fishes = [], bubbles = [], seaweeds = [], backSeaweeds = [], farSeaweeds = [], motes = [], rays = [], rocks = [], grass = [], fgGrass = [];
87
  let playerFish = null; // the fish controlled by gamepad (spawned with F key)
88
  const GAMEPAD_DEADZONE = 0.15; // ignore small stick movements
89
  let curNight = 0; // v7: shared night factor (dims the top-light + sheen)
 
894
  bubbles = [];
895
  seaweeds = [];
896
  backSeaweeds = []; // v4: blurry background seaweed (not fish-touchable)
897
+ farSeaweeds = []; // v5: ultra-far pre-rendered blurry sprites (deepest layer)
898
  motes = [];
899
  rays = [];
900
  rocks = [];
 
1009
  const nStalks = 2 + ((Math.random() * 3) | 0); // 2..4 stalks per bundle
1010
  for (let j = 0; j < nStalks; j++) {
1011
  const hue = clusterHue + rand(-15, 15);
1012
+ // v4.3: depth split — ~51% "far" (heavy softness), ~49% "near" (sharper) for a layered look
1013
+ const near = Math.random() < 0.49;
 
1014
  const st = {
1015
  x: cx + rand(-20, 20),
1016
  h: rand(H * 0.35, H * 0.65), // much taller than foreground seaweed
 
1019
  speed: rand(0.7, 1.3), // slower, more gentle sway
1020
  hue,
1021
  near,
 
1022
  width: near ? rand(2.5, 5) : rand(3, 6),
1023
  tipW: near ? rand(1.2, 2.4) : rand(1.5, 3),
1024
  branches: [],
1025
  // organic curve parameters (same as foreground seaweed)
1026
  cA1: rand(8, 15), cF1: rand(0.8, 1.6), cP1: rand(0, Math.PI * 2),
1027
  cA2: rand(3, 8), cF2: rand(2.0, 3.2), cP2: rand(0, Math.PI * 2),
1028
+ // v4.3: multi-wave organic motion parameters
1029
+ waveA1: rand(6, 14), waveF1: rand(0.5, 1.2), wavePh1: rand(0, Math.PI * 2),
1030
+ waveA2: rand(3, 8), waveF2: rand(1.3, 2.1), wavePh2: rand(0, Math.PI * 2),
1031
+ waveA3: rand(2, 5), waveF3: rand(2.4, 3.6), wavePh3: rand(0, Math.PI * 2)
1032
  };
1033
  // v4.1: add branches to background seaweed (like foreground)
1034
  const nBr = 1 + ((Math.random() * 3) | 0); // 1..3 branches per stalk
 
1046
  backSeaweeds.push(st);
1047
  }
1048
  }
1049
+
1050
+ // v5: ultra-far pre-rendered blurry seaweed — deepest layer, heavily blurred sprites
1051
+ const nFarClusters = Math.max(6, Math.floor(W / 180)); // 50% more than previous far count
1052
+ for (let i = 0; i < nFarClusters; i++) {
1053
+ const cx = rand(20, W - 20);
1054
+ const clusterHue = rand(170, 210); // very blue-tinted for depth
1055
+ const nStalks = 2 + ((Math.random() * 3) | 0); // 2..4 stalks per bundle
1056
+ for (let j = 0; j < nStalks; j++) {
1057
+ const hue = clusterHue + rand(-15, 15);
1058
+ const st = {
1059
+ x: cx + rand(-25, 25),
1060
+ h: rand(H * 0.4, H * 0.7), // very tall
1061
+ segs: 8,
1062
+ phase: rand(0, Math.PI * 2),
1063
+ speed: rand(0.5, 1.0), // very slow sway
1064
+ hue,
1065
+ width: rand(4, 8),
1066
+ tipW: rand(2, 4),
1067
+ branches: [],
1068
+ sprite: null, // will be built by buildFarSeaweedSprites()
1069
+ spriteXOffset: 0,
1070
+ spriteYOffset: 0
1071
+ };
1072
+ // Add some branches for visual complexity (will be part of the sprite)
1073
+ const nBr = 1 + ((Math.random() * 2) | 0); // 1..2 branches
1074
+ for (let k = 0; k < nBr; k++) {
1075
+ st.branches.push({
1076
+ f: rand(0.3, 0.8),
1077
+ side: Math.random() < 0.5 ? -1 : 1,
1078
+ len: rand(12, 28),
1079
+ up: rand(10, 24),
1080
+ w: rand(2, 4),
1081
+ ph: rand(0, Math.PI * 2)
1082
+ });
1083
+ }
1084
+ farSeaweeds.push(st);
1085
+ }
1086
+ }
1087
 
1088
  // Short, dense grass-like ground cover along the sand bed.
1089
  // Each tuft is a cluster of thin blades that ripple when fish swim nearby.
 
1208
  canvas.height = H * dpr;
1209
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
1210
  buildScene();
1211
+ buildFarSeaweedSprites(); // v5: pre-render ultra-far blurry seaweed sprites
1212
  }
1213
 
1214
  // --- slow light cycle: bright day -> dusk -> night -> dawn, over CYCLE seconds ---
 
1485
  return f * (w.cA1 * Math.sin(f * w.cF1 + w.cP1) + w.cA2 * Math.sin(f * w.cF2 + w.cP2));
1486
  }
1487
 
1488
+ // v4.3: draw background seaweed procedurally — multi-wave organic motion instead of
1489
+ // rigid sprite rotation. Far stalks get a soft low-alpha halo pass to keep their
1490
+ // "far away" look without any runtime blur filters (no slowdown).
1491
+ function drawBackSeaweeds(t) {
1492
+ ctx.save();
1493
+ ctx.filter = 'blur(2px)'; // v5.1: added softness to nearer background seaweed
1494
  for (const w of backSeaweeds) {
1495
+ const baseY = sandAt(w.x) + 10;
1496
+ const n = w.segs;
1497
+ const coreStyle = `hsla(${w.hue},50%,32%,${w.near ? 0.8 : 0.7})`;
1498
+ const haloStyle = `hsla(${w.hue},45%,36%,0.18)`;
1499
+
1500
+ // stalk sway at fraction f: three overlapping waves, root planted (f^2), tip free (f)
1501
+ const swayAt = (f) =>
1502
+ Math.sin(t * w.waveF1 + w.wavePh1 + f * 2.4) * w.waveA1 * f * f +
1503
+ Math.sin(t * w.waveF2 + w.wavePh2 + f * 3.8) * w.waveA2 * f * f +
1504
+ Math.sin(t * w.waveF3 + w.wavePh3 + f * 5.4) * w.waveA3 * f;
1505
+
1506
+ for (let pass = 0; pass < (w.near ? 1 : 2); pass++) {
1507
+ const isHalo = !w.near && pass === 0;
1508
+ ctx.strokeStyle = isHalo ? haloStyle : coreStyle;
1509
+ ctx.lineCap = 'round';
1510
+ ctx.lineJoin = 'round';
1511
+ ctx.lineWidth = (w.width + w.tipW) / 2 * (isHalo ? 1.9 : 1);
1512
+
1513
+ // stalk outline: up one side, round tip, down the other
1514
+ ctx.beginPath();
1515
+ let prevX = 0, prevY = baseY;
1516
+ let lastPx = 0, lastPy = baseY; // track final position for tip cap
1517
+ for (let i = 0; i <= n; i++) {
1518
+ const f = i / n;
1519
+ const px = w.x + stalkCurveX(w, f) + swayAt(f);
1520
+ const py = baseY - w.h * f;
1521
+ const dx = px - prevX, dy = py - prevY;
1522
+ const dl = Math.hypot(dx, dy) || 1;
1523
+ const nx = -dy / dl, ny = dx / dl;
1524
+ const hw = (w.width * (1 - f) + w.tipW * f) / 2;
1525
+ if (i === 0) ctx.moveTo(px + nx * hw, py + ny * hw);
1526
+ else ctx.lineTo(px + nx * hw, py + ny * hw);
1527
+ prevX = px; prevY = py;
1528
+ lastPx = px; lastPy = py;
1529
+ }
1530
+ // round tip cap (follows the stalk's actual direction)
1531
+ const dxT = lastPx - prevX, dyT = lastPy - prevY;
1532
+ const dlT = Math.hypot(dxT, dyT) || 1;
1533
+ const nxT = -dyT / dlT, nyT = dxT / dlT;
1534
+ ctx.quadraticCurveTo(lastPx, lastPy, lastPx + dxT * 0.5 + nxT * (w.tipW / 2), lastPy + dyT * 0.5 + nyT * (w.tipW / 2));
1535
+ // down the other side
1536
+ for (let i = n; i >= 0; i--) {
1537
+ const f = i / n;
1538
+ const px = w.x + stalkCurveX(w, f) + swayAt(f);
1539
+ const py = baseY - w.h * f;
1540
+ // perpendicular from this segment direction
1541
+ let dx, dy;
1542
+ if (i === n) { dx = prevX - px; dy = prevY - py; }
1543
+ else {
1544
+ const f2 = (i + 1) / n;
1545
+ const px2 = w.x + stalkCurveX(w, f2) + swayAt(f2);
1546
+ const py2 = baseY - w.h * f2;
1547
+ dx = px2 - px; dy = py2 - py;
1548
+ }
1549
+ const dl = Math.hypot(dx, dy) || 1;
1550
+ const nx = -dy / dl, ny = dx / dl;
1551
+ const hw = (w.width * (1 - f) + w.tipW * f) / 2;
1552
+ ctx.lineTo(px - nx * hw, py - ny * hw);
1553
+ }
1554
+ ctx.closePath();
1555
+ ctx.stroke();
1556
+
1557
+ // branches flutter with the stalk
1558
+ for (const b of w.branches) {
1559
+ const idx = Math.min(n, Math.max(1, Math.round(b.f * n)));
1560
+ const f = idx / n;
1561
+ const ax = w.x + stalkCurveX(w, f) + swayAt(f);
1562
+ const ay = baseY - w.h * f;
1563
+ const flutter = Math.sin(t * w.waveF2 * 1.4 + b.ph) * (3 + w.waveA2 * 0.4);
1564
+ const bx = ax + b.side * b.len + flutter;
1565
+ const by = ay - b.up;
1566
+ ctx.beginPath();
1567
+ ctx.lineWidth = isHalo ? b.w * 1.9 : b.w;
1568
+ ctx.strokeStyle = isHalo ? haloStyle : `hsla(${w.hue + 8},55%,42%,0.75)`;
1569
+ ctx.moveTo(ax, ay);
1570
+ ctx.bezierCurveTo(
1571
+ ax + b.side * b.len * 0.3, ay - b.up * 0.75,
1572
+ bx - b.side * b.len * 0.35, by + b.up * 0.6,
1573
+ bx, by
1574
+ );
1575
+ ctx.stroke();
1576
+ }
1577
+ }
1578
+ }
1579
+ ctx.restore();
1580
+ }
1581
+
1582
+ // v5: pre-render ultra-far seaweed sprites with heavy blur baked in at build time
1583
+ function buildFarSeaweedSprites() {
1584
+ const BLUR = 7; // v5.1: reduced from 10 (33% less) — slightly closer appearance
1585
+ const PAD = 40; // extra space for blur spill
1586
+
1587
+ for (const w of farSeaweeds) {
1588
+ // Calculate bounding box by sampling the stalk curve
1589
+ const baseY = sandAt(w.x) + 10;
1590
  let xMin = Infinity, xMax = -Infinity;
1591
 
 
1592
  for (let i = 0; i <= w.segs; i++) {
1593
  const f = i / w.segs;
1594
+ // Use a static curve for the sprite (no animation baked in)
1595
+ const px = w.x + f * 10 * Math.sin(f * 2.5 + w.phase); // gentle curve
 
 
 
 
 
 
1596
  if (px < xMin) xMin = px;
1597
  if (px > xMax) xMax = px;
1598
  }
 
1610
  spr.height = bh;
1611
  const sc = spr.getContext("2d");
1612
 
1613
+ // Apply heavy blur filter (baked in at build time - zero runtime cost)
 
1614
  sc.filter = `blur(${BLUR}px)`;
1615
 
1616
+ // Draw the stalk as a tapered blade with branches
1617
+ const coreStyle = `hsla(${w.hue},45%,28%,0.65)`;
1618
+ sc.strokeStyle = coreStyle;
1619
+ sc.lineCap = 'round';
1620
+ sc.lineJoin = 'round';
1621
+
1622
+ // Calculate sprite-local coordinates
1623
+ const localBaseX = bw / 2;
1624
+ const localBaseY = bh - PAD;
1625
+
1626
+ // Draw main stalk outline
1627
+ sc.beginPath();
1628
+ let prevX = localBaseX, prevY = localBaseY;
1629
+
1630
  for (let i = 0; i <= w.segs; i++) {
1631
  const f = i / w.segs;
1632
+ const curveX = f * 10 * Math.sin(f * 2.5 + w.phase);
1633
+ const px = localBaseX + curveX;
1634
+ const py = localBaseY - w.h * f;
1635
+
1636
+ if (i === 0) sc.moveTo(px, py);
1637
+ else sc.lineTo(px, py);
1638
+ prevX = px; prevY = py;
1639
  }
1640
 
1641
+ // Draw with width to create the blade shape
1642
+ sc.lineWidth = (w.width + w.tipW) / 2;
1643
+ sc.stroke();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1644
 
1645
+ // Draw branches
1646
  for (const b of w.branches) {
1647
  const idx = Math.min(w.segs, Math.max(1, Math.round(b.f * w.segs)));
1648
+ const f = idx / w.segs;
1649
+ const curveX = f * 10 * Math.sin(f * 2.5 + w.phase);
1650
+ const ax = localBaseX + curveX;
1651
+ const ay = localBaseY - w.h * f;
 
1652
 
 
 
 
 
 
 
 
 
 
 
 
 
1653
  sc.beginPath();
1654
+ sc.lineWidth = b.w;
1655
+ sc.strokeStyle = `hsla(${w.hue + 8},40%,32%,0.5)`;
1656
+ sc.moveTo(ax, ay);
1657
+ const bx = ax + b.side * b.len;
1658
+ const by = ay - b.up;
1659
+ sc.bezierCurveTo(
1660
+ ax + b.side * b.len * 0.3, ay - b.up * 0.75,
1661
+ bx - b.side * b.len * 0.35, by + b.up * 0.6,
1662
+ bx, by
1663
+ );
1664
  sc.stroke();
1665
  }
1666
 
1667
+ // Store sprite and offset for drawing
 
1668
  w.sprite = spr;
1669
+ w.spriteXOffset = xMin; // left edge of bounding box in world coords
1670
+ w.spriteYOffset = baseY - bh + PAD; // top edge in world coords (baseY is bottom)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1671
  }
1672
  }
1673
 
1674
+ // v5: draw ultra-far seaweed sprites with simple sway animation
1675
+ function drawFarSeaweeds(t) {
1676
+ for (const w of farSeaweeds) {
1677
  if (!w.sprite) continue;
1678
+
1679
  const baseY = sandAt(w.x) + 10;
1680
+ // Simple rotational sway - root stays planted, sprite rotates slightly
1681
+ const swayAngle = Math.sin(t * w.speed + w.phase) * 0.04; // very subtle rotation
1682
+
 
 
 
1683
  ctx.save();
1684
+ // Translate to the base point (where the stalk meets the sand)
1685
+ ctx.translate(w.x, baseY);
1686
+ // Rotate around the base point for natural sway
1687
+ ctx.rotate(swayAngle);
1688
+ // Draw sprite at its offset position relative to base
1689
+ ctx.drawImage(w.sprite, w.spriteXOffset - w.x, w.spriteYOffset - baseY);
1690
  ctx.restore();
1691
  }
1692
  }
 
2088
  drawRays(t, light);
2089
  updateMotes(dt, t);
2090
 
2091
+ // v5: ultra-far pre-rendered blurry seaweed — deepest layer, behind everything else
2092
+ drawFarSeaweeds(t);
2093
+
2094
  // v4: background seaweed — drawn behind sand/rocks for depth
2095
+ drawBackSeaweeds(t); // v4.3: procedural multi-wave motion (no separate update pass)
 
2096
 
2097
  updatePlants(t, dt);
2098
  updateGrass(t, dt);
 
2162
  function doResize() {
2163
  resize();
2164
  buildRaySprites(); // v23: rebuild the pre-blurred beam sprites for the new size
2165
+ buildFarSeaweedSprites(); // v5: rebuild ultra-far seaweed sprites for new size
2166
  buildStaticLayer();
2167
  if (!grainPattern) buildGrainTile(); // v8: grain tile is size-independent, build once
2168
  }