Upload index.html
Browse files- 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.
|
| 1012 |
-
|
| 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 |
-
//
|
| 1030 |
-
|
| 1031 |
-
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
| 1451 |
-
|
| 1452 |
-
|
| 1453 |
-
|
| 1454 |
-
|
|
|
|
| 1455 |
for (const w of backSeaweeds) {
|
| 1456 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 1463 |
-
|
| 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
|
| 1488 |
-
const BLUR = w.blur || BLUR_MAX;
|
| 1489 |
sc.filter = `blur(${BLUR}px)`;
|
| 1490 |
|
| 1491 |
-
// Draw the stalk as a tapered blade
|
| 1492 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1493 |
for (let i = 0; i <= w.segs; i++) {
|
| 1494 |
const f = i / w.segs;
|
| 1495 |
-
const
|
| 1496 |
-
|
| 1497 |
-
|
| 1498 |
-
|
| 1499 |
-
|
| 1500 |
-
|
|
|
|
| 1501 |
}
|
| 1502 |
|
| 1503 |
-
// Draw
|
| 1504 |
-
sc.
|
| 1505 |
-
sc.
|
| 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 |
-
//
|
| 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
|
| 1544 |
-
|
| 1545 |
-
|
| 1546 |
-
const
|
| 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.
|
| 1563 |
-
sc.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1564 |
sc.stroke();
|
| 1565 |
}
|
| 1566 |
|
| 1567 |
-
// Store sprite and
|
| 1568 |
-
// BUG FIX: store RELATIVE offset from stalk base, not absolute screen position
|
| 1569 |
w.sprite = spr;
|
| 1570 |
-
w.spriteXOffset = xMin
|
| 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 |
-
//
|
| 1590 |
-
function
|
| 1591 |
-
for (const w of
|
| 1592 |
if (!w.sprite) continue;
|
| 1593 |
-
|
| 1594 |
const baseY = sandAt(w.x) + 10;
|
| 1595 |
-
|
| 1596 |
-
|
| 1597 |
-
|
| 1598 |
-
const swayA = w.currentSway * 0.035 + w.currentSway2 * 0.018;
|
| 1599 |
-
const swayX = w.currentSway * 3;
|
| 1600 |
-
|
| 1601 |
ctx.save();
|
| 1602 |
-
|
| 1603 |
-
ctx.
|
| 1604 |
-
//
|
| 1605 |
-
ctx.
|
|
|
|
|
|
|
| 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 |
-
|
| 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 |
-
|
| 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 |
}
|