Upload index.html
Browse files- index.html +170 -68
index.html
CHANGED
|
@@ -85,8 +85,9 @@ function genKoiPatches(type) {
|
|
| 85 |
}
|
| 86 |
let fishes = [], bubbles = [], seaweeds = [], motes = [], rays = [], rocks = [], grass = [], fgGrass = [];
|
| 87 |
let curNight = 0; // v7: shared night factor (dims the top-light + sheen)
|
| 88 |
-
let plantPoints = []; // collision points
|
| 89 |
let sandHeights = [];
|
|
|
|
| 90 |
|
| 91 |
class Fish {
|
| 92 |
constructor() {
|
|
@@ -865,6 +866,8 @@ function buildScene() {
|
|
| 865 |
rays = [];
|
| 866 |
rocks = [];
|
| 867 |
sandHeights = [];
|
|
|
|
|
|
|
| 868 |
grass = []; // v10.1: clear on rebuild so resize/F11 doesn't stack a second row
|
| 869 |
fgGrass = []; // v12: same reset for the foreground layer (resize/F11 safety)
|
| 870 |
|
|
@@ -929,7 +932,9 @@ function buildScene() {
|
|
| 929 |
off: new Float32Array(9), // lateral bend per segment (fish push leaves aside)
|
| 930 |
offV: new Float32Array(9), // velocity of that bend
|
| 931 |
branches: [],
|
| 932 |
-
|
|
|
|
|
|
|
| 933 |
};
|
| 934 |
const nBr = 1 + ((Math.random() * 3) | 0); // 1..3 branches per stalk
|
| 935 |
for (let k = 0; k < nBr; k++) {
|
|
@@ -971,6 +976,7 @@ function buildScene() {
|
|
| 971 |
const baseHue = rand(95, 155);
|
| 972 |
const blades = 2 + ((Math.random() * 4) | 0); // 2..5 blades per tuft
|
| 973 |
for (let b = 0; b < blades; b++) {
|
|
|
|
| 974 |
grass.push({
|
| 975 |
x: gx + rand(-7, 7),
|
| 976 |
y: gy,
|
|
@@ -985,11 +991,14 @@ function buildScene() {
|
|
| 985 |
cA1: rand(-5, 5), // primary bend amplitude
|
| 986 |
cF1: rand(2.2, 4.5), // frequency along blade length
|
| 987 |
cP1: rand(0, Math.PI * 2), // phase offset
|
| 988 |
-
segs:
|
| 989 |
// v17: secondary wobble - finer inner shimmer per blade
|
| 990 |
sAmp: rand(1.5, 3.5),
|
| 991 |
sFreq: rand(4.5, 8),
|
| 992 |
-
sPh: rand(0, Math.PI * 2)
|
|
|
|
|
|
|
|
|
|
| 993 |
});
|
| 994 |
}
|
| 995 |
}
|
|
@@ -1005,6 +1014,7 @@ function buildScene() {
|
|
| 1005 |
const baseHue = rand(46, 76); // v12.1: yellowish-green β pops against the mid-ground green grass
|
| 1006 |
const nBlades = 3 + ((Math.random() * 4) | 0); // 3..6 blades per clump
|
| 1007 |
for (let b = 0; b < nBlades; b++) {
|
|
|
|
| 1008 |
fgGrass.push({
|
| 1009 |
x: cx + rand(-7.5, 7.5), // 50%: tighter clumps (was -15..15)
|
| 1010 |
y: clusterBaseY + rand(-2, 4), // 50%: finer vertical jitter (was -4..8)
|
|
@@ -1018,15 +1028,23 @@ function buildScene() {
|
|
| 1018 |
cA1: rand(-8, 8), // primary bend amplitude (larger for foreground)
|
| 1019 |
cF1: rand(2, 4), // frequency along blade length
|
| 1020 |
cP1: rand(0, Math.PI * 2), // phase offset
|
| 1021 |
-
segs:
|
| 1022 |
// v17: secondary wobble - finer inner shimmer per blade (foreground)
|
| 1023 |
sAmp: rand(2, 4.5),
|
| 1024 |
sFreq: rand(3.8, 6.5),
|
| 1025 |
-
sPh: rand(0, Math.PI * 2)
|
|
|
|
|
|
|
| 1026 |
});
|
| 1027 |
}
|
| 1028 |
}
|
| 1029 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1030 |
const nFish = Math.max(7, Math.floor(W / 130));
|
| 1031 |
for (let i = 0; i < nFish; i++) fishes.push(new Fish());
|
| 1032 |
|
|
@@ -1038,9 +1056,9 @@ function buildScene() {
|
|
| 1038 |
}
|
| 1039 |
|
| 1040 |
for (let i = 0; i < 5; i++) {
|
| 1041 |
-
// random blur state: ~
|
| 1042 |
const blurRoll = Math.random();
|
| 1043 |
-
const blur = blurRoll < 0.34 ? 3 : (blurRoll < 0.67 ?
|
| 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,
|
|
@@ -1140,73 +1158,145 @@ function drawBackground(light) {
|
|
| 1140 |
ctx.fillRect(0, 0, W, H);
|
| 1141 |
}
|
| 1142 |
|
| 1143 |
-
//
|
| 1144 |
-
//
|
| 1145 |
-
//
|
| 1146 |
-
|
| 1147 |
-
|
| 1148 |
-
|
| 1149 |
-
|
| 1150 |
-
|
| 1151 |
-
|
| 1152 |
-
|
| 1153 |
-
|
| 1154 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1155 |
for (const r of rays) {
|
| 1156 |
-
// apply per-ray blur state: 0px (as-is), 3px (semi-blurry), or 7px (blurry)
|
| 1157 |
-
ctx.filter = r.blur > 0 ? `blur(${r.blur}px)` : 'none';
|
| 1158 |
-
// slow horizontal drift of the whole beam (kept from v1)
|
| 1159 |
-
const off = Math.sin(t * r.speed * 6 + r.x) * 55;
|
| 1160 |
-
const x0 = r.x + off;
|
| 1161 |
-
const topW = r.w * 0.45, botW = r.w * 1.9;
|
| 1162 |
const shift = r.tilt * H;
|
|
|
|
| 1163 |
|
| 1164 |
-
//
|
| 1165 |
-
//
|
| 1166 |
-
|
| 1167 |
-
|
| 1168 |
-
|
| 1169 |
-
const
|
| 1170 |
-
const
|
| 1171 |
-
const
|
| 1172 |
-
|
| 1173 |
-
|
| 1174 |
-
Math.sin(v * r.vLen - t * r.vSpd + r.vPh) * r.vAmp;
|
| 1175 |
-
const cx = x0 + shift * v + wave * grow; // undulating center x at this depth
|
| 1176 |
-
const halfW = topW + (botW - topW) * v; // beam widens as it falls
|
| 1177 |
-
L[i][0] = cx - halfW; L[i][1] = y;
|
| 1178 |
-
R[i][0] = cx + halfW; R[i][1] = y;
|
| 1179 |
}
|
| 1180 |
-
|
| 1181 |
-
|
| 1182 |
-
const
|
| 1183 |
-
|
| 1184 |
-
|
| 1185 |
-
|
| 1186 |
-
|
| 1187 |
-
|
| 1188 |
-
|
| 1189 |
-
|
| 1190 |
-
|
| 1191 |
-
|
| 1192 |
-
|
| 1193 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1194 |
};
|
| 1195 |
|
| 1196 |
-
//
|
| 1197 |
-
|
| 1198 |
-
|
| 1199 |
-
|
| 1200 |
-
const
|
| 1201 |
-
|
| 1202 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1203 |
const cxm = (lx + rx2) / 2;
|
| 1204 |
const coreHalf = ((rx2 - lx) / 2) * 0.42; // 42% of the local half-width, as in v1
|
| 1205 |
-
LC[
|
| 1206 |
-
RC[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1207 |
}
|
| 1208 |
-
band(LC, RC, r.alpha * 1.5, 0.65, 0);
|
| 1209 |
-
ctx.filter = 'none'; // reset blur before next ray or other drawing
|
| 1210 |
}
|
| 1211 |
}
|
| 1212 |
|
|
@@ -1605,10 +1695,17 @@ function buildGrainTile() {
|
|
| 1605 |
g.putImageData(img, 0, 0);
|
| 1606 |
grainPattern = ctx.createPattern(tile, "repeat"); // cached once, reused every frame
|
| 1607 |
}
|
|
|
|
| 1608 |
function drawGrain() {
|
| 1609 |
if (!grainPattern) return;
|
| 1610 |
-
|
| 1611 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1612 |
ctx.save();
|
| 1613 |
ctx.globalCompositeOperation = "overlay";
|
| 1614 |
ctx.globalAlpha = 0.7; // mild: subtle shimmer, not heavy noise
|
|
@@ -1621,6 +1718,10 @@ function drawGrain() {
|
|
| 1621 |
// Precomputed static layers β the sand and rocks never move, so they are
|
| 1622 |
// rendered once to an offscreen canvas and blitted each frame (v6) instead of
|
| 1623 |
// re-drawing their gradients and paths every single frame.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1624 |
let staticLayer = null;
|
| 1625 |
function buildStaticLayer() {
|
| 1626 |
staticLayer = document.createElement("canvas");
|
|
@@ -1711,6 +1812,7 @@ function frame(now) {
|
|
| 1711 |
|
| 1712 |
function doResize() {
|
| 1713 |
resize();
|
|
|
|
| 1714 |
buildStaticLayer();
|
| 1715 |
if (!grainPattern) buildGrainTile(); // v8: grain tile is size-independent, build once
|
| 1716 |
}
|
|
|
|
| 85 |
}
|
| 86 |
let fishes = [], bubbles = [], seaweeds = [], motes = [], rays = [], rocks = [], grass = [], fgGrass = [];
|
| 87 |
let curNight = 0; // v7: shared night factor (dims the top-light + sheen)
|
| 88 |
+
let plantPoints = [], _ppUsed = 0; // collision points β a pooled set, reused in place
|
| 89 |
let sandHeights = [];
|
| 90 |
+
let sandParticles = []; // v22: grains of sand kicked up when a fast fish skims the sea floor
|
| 91 |
|
| 92 |
class Fish {
|
| 93 |
constructor() {
|
|
|
|
| 866 |
rays = [];
|
| 867 |
rocks = [];
|
| 868 |
sandHeights = [];
|
| 869 |
+
sandParticles = []; // v22: fresh bed, no leftover grains
|
| 870 |
+
raySpritesBuiltFor = null; // v23: beam sprites are sized to the old canvas β rebuild them
|
| 871 |
grass = []; // v10.1: clear on rebuild so resize/F11 doesn't stack a second row
|
| 872 |
fgGrass = []; // v12: same reset for the foreground layer (resize/F11 safety)
|
| 873 |
|
|
|
|
| 932 |
off: new Float32Array(9), // lateral bend per segment (fish push leaves aside)
|
| 933 |
offV: new Float32Array(9), // velocity of that bend
|
| 934 |
branches: [],
|
| 935 |
+
// PERF: stalk points preallocated once β updatePlants reuses these objects
|
| 936 |
+
// in place instead of pushing fresh {x,y} pairs every frame (GC churn)
|
| 937 |
+
points: Array.from({ length: 9 }, () => ({ x: 0, y: 0 })) // segs+1; can't read st inside its own initializer
|
| 938 |
};
|
| 939 |
const nBr = 1 + ((Math.random() * 3) | 0); // 1..3 branches per stalk
|
| 940 |
for (let k = 0; k < nBr; k++) {
|
|
|
|
| 976 |
const baseHue = rand(95, 155);
|
| 977 |
const blades = 2 + ((Math.random() * 4) | 0); // 2..5 blades per tuft
|
| 978 |
for (let b = 0; b < blades; b++) {
|
| 979 |
+
const bladeSegs = 3 + ((Math.random() * 2) | 0); // 3-4 segments for tapered outline
|
| 980 |
grass.push({
|
| 981 |
x: gx + rand(-7, 7),
|
| 982 |
y: gy,
|
|
|
|
| 991 |
cA1: rand(-5, 5), // primary bend amplitude
|
| 992 |
cF1: rand(2.2, 4.5), // frequency along blade length
|
| 993 |
cP1: rand(0, Math.PI * 2), // phase offset
|
| 994 |
+
segs: bladeSegs, // 3-4 segments for tapered outline
|
| 995 |
// v17: secondary wobble - finer inner shimmer per blade
|
| 996 |
sAmp: rand(1.5, 3.5),
|
| 997 |
sFreq: rand(4.5, 8),
|
| 998 |
+
sPh: rand(0, Math.PI * 2),
|
| 999 |
+
// PERF: per-blade scratch points β drawGrass reuses them instead of
|
| 1000 |
+
// allocating a fresh pts array + point objects every frame (GC churn)
|
| 1001 |
+
pts: Array.from({ length: bladeSegs + 1 }, () => ({ x: 0, y: 0 }))
|
| 1002 |
});
|
| 1003 |
}
|
| 1004 |
}
|
|
|
|
| 1014 |
const baseHue = rand(46, 76); // v12.1: yellowish-green β pops against the mid-ground green grass
|
| 1015 |
const nBlades = 3 + ((Math.random() * 4) | 0); // 3..6 blades per clump
|
| 1016 |
for (let b = 0; b < nBlades; b++) {
|
| 1017 |
+
const fgSegs = 4 + ((Math.random() * 2) | 0); // 4-5 segments for tapered outline
|
| 1018 |
fgGrass.push({
|
| 1019 |
x: cx + rand(-7.5, 7.5), // 50%: tighter clumps (was -15..15)
|
| 1020 |
y: clusterBaseY + rand(-2, 4), // 50%: finer vertical jitter (was -4..8)
|
|
|
|
| 1028 |
cA1: rand(-8, 8), // primary bend amplitude (larger for foreground)
|
| 1029 |
cF1: rand(2, 4), // frequency along blade length
|
| 1030 |
cP1: rand(0, Math.PI * 2), // phase offset
|
| 1031 |
+
segs: fgSegs, // 4-5 segments for tapered outline
|
| 1032 |
// v17: secondary wobble - finer inner shimmer per blade (foreground)
|
| 1033 |
sAmp: rand(2, 4.5),
|
| 1034 |
sFreq: rand(3.8, 6.5),
|
| 1035 |
+
sPh: rand(0, Math.PI * 2),
|
| 1036 |
+
// PERF: per-blade scratch points β drawFgGrass reuses them in place
|
| 1037 |
+
pts: Array.from({ length: fgSegs + 1 }, () => ({ x: 0, y: 0 }))
|
| 1038 |
});
|
| 1039 |
}
|
| 1040 |
}
|
| 1041 |
|
| 1042 |
+
// PERF: pool the plantPoints collision objects too β updatePlants reuses them
|
| 1043 |
+
// in place instead of allocating one per stalk/branch point every frame.
|
| 1044 |
+
let pcap = 8;
|
| 1045 |
+
for (const w of seaweeds) pcap += (w.segs + 1) + w.branches.length * 2;
|
| 1046 |
+
while (plantPoints.length < pcap) plantPoints.push({ x: 0, y: 0, r: 0, s: null });
|
| 1047 |
+
|
| 1048 |
const nFish = Math.max(7, Math.floor(W / 130));
|
| 1049 |
for (let i = 0; i < nFish; i++) fishes.push(new Fish());
|
| 1050 |
|
|
|
|
| 1056 |
}
|
| 1057 |
|
| 1058 |
for (let i = 0; i < 5; i++) {
|
| 1059 |
+
// random blur state: ~3.75px (semi-blurry), ~8.75px (blurry), or ~17.5px (very blurry) for visual variety
|
| 1060 |
const blurRoll = Math.random();
|
| 1061 |
+
const blur = blurRoll < 0.34 ? 3.75 : (blurRoll < 0.67 ? 8.75 : 17.5);
|
| 1062 |
rays.push({
|
| 1063 |
x: rand(0, W), w: rand(80, 210), tilt: rand(-0.35, 0.35),
|
| 1064 |
speed: rand(0.06, 0.14) * (Math.random() < 0.5 ? 1 : -1), alpha: rand(0.04, 0.09), blur,
|
|
|
|
| 1158 |
ctx.fillRect(0, 0, W, H);
|
| 1159 |
}
|
| 1160 |
|
| 1161 |
+
// ============ Light rays β pre-blurred sprites (v23 fast path) ============
|
| 1162 |
+
// The old version applied a canvas blur() filter to every beam on every frame;
|
| 1163 |
+
// canvas filters force slow CPU rasterization over large regions, which tanked FPS.
|
| 1164 |
+
// Now each beam's shape is rendered ONCE into its own offscreen sprite (blur + the
|
| 1165 |
+
// vertical fade baked in at build time), and per frame we just blit that sprite in
|
| 1166 |
+
// horizontal slices with the same undulation offsets as before β wobble + soft
|
| 1167 |
+
// edges, zero filters at runtime.
|
| 1168 |
+
const RAY_SEGS = 18; // vertical resolution of each beam's spine (at build time)
|
| 1169 |
+
const RAY_SLICES = 24; // horizontal slices per beam used for the wobble (per frame)
|
| 1170 |
+
const RAY_SPRITE_SCALE = 0.5; // sprites stored at half resolution β softer + cheaper
|
| 1171 |
+
const RAY_PAD = 60; // room for the blur to spill past the beam edge
|
| 1172 |
+
let raySpritesBuiltFor = null;
|
| 1173 |
+
let tintCanvas = null, tctx = null;
|
| 1174 |
+
|
| 1175 |
+
function buildRaySprites() {
|
| 1176 |
+
const S = RAY_SPRITE_SCALE;
|
| 1177 |
+
const yTop0 = -10, ySpan = H * 0.95 + 10; // beam vertical extent (same as before)
|
| 1178 |
+
|
| 1179 |
+
let maxTw = 0, maxTh = 0;
|
| 1180 |
for (const r of rays) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1181 |
const shift = r.tilt * H;
|
| 1182 |
+
const topW = r.w * 0.45, botW = r.w * 1.9;
|
| 1183 |
|
| 1184 |
+
// static spine in local coords (x relative to the beam origin); the per-frame
|
| 1185 |
+
// drift + wobble are added when slicing, so each sprite stays reusable
|
| 1186 |
+
const L = [], Rr = [];
|
| 1187 |
+
for (let i = 0; i <= RAY_SEGS; i++) {
|
| 1188 |
+
const v = i / RAY_SEGS;
|
| 1189 |
+
const y = yTop0 + v * ySpan;
|
| 1190 |
+
const cx = shift * v; // centerline x at this depth
|
| 1191 |
+
const halfW = topW + (botW - topW) * v; // beam widens as it falls
|
| 1192 |
+
L.push([cx - halfW, y]);
|
| 1193 |
+
Rr.push([cx + halfW, y]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1194 |
}
|
| 1195 |
+
let xMinL = Infinity, xMaxL = -Infinity;
|
| 1196 |
+
for (const [px] of L) if (px < xMinL) xMinL = px;
|
| 1197 |
+
for (const [px] of Rr) if (px > xMaxL) xMaxL = px;
|
| 1198 |
+
|
| 1199 |
+
const bw = Math.ceil(xMaxL - xMinL + RAY_PAD * 2);
|
| 1200 |
+
const bh = Math.ceil(ySpan + RAY_PAD * 2);
|
| 1201 |
+
|
| 1202 |
+
// draw the beam into a full-res scratch canvas (identity transform so the blur
|
| 1203 |
+
// radius is unambiguous), then downscale it into the half-res sprite
|
| 1204 |
+
const full = document.createElement("canvas");
|
| 1205 |
+
full.width = bw; full.height = bh;
|
| 1206 |
+
const fc = full.getContext("2d");
|
| 1207 |
+
fc.translate(-xMinL + RAY_PAD, -yTop0 + RAY_PAD); // local beam coords β scratch px
|
| 1208 |
+
if (r.blur > 0) { try { fc.filter = `blur(${r.blur}px)`; } catch (e) {} }
|
| 1209 |
+
|
| 1210 |
+
const poly = (a, b) => {
|
| 1211 |
+
fc.beginPath();
|
| 1212 |
+
fc.moveTo(a[0][0], a[0][1]);
|
| 1213 |
+
for (let i = 1; i < a.length; i++) fc.lineTo(a[i][0], a[i][1]);
|
| 1214 |
+
for (let i = b.length - 1; i >= 0; i--) fc.lineTo(b[i][0], b[i][1]);
|
| 1215 |
+
fc.closePath();
|
| 1216 |
+
fc.fill();
|
| 1217 |
};
|
| 1218 |
|
| 1219 |
+
// vertical fade baked into the sprite: bright at the surface, gone by the floor
|
| 1220 |
+
// (same gradient stops as before, mapped into scratch-canvas coordinates)
|
| 1221 |
+
const yS0 = -yTop0 + RAY_PAD; // scratch y of world y=0
|
| 1222 |
+
const yS1 = H - yTop0 + RAY_PAD; // scratch y of world y=H
|
| 1223 |
+
const gOuter = fc.createLinearGradient(0, yS0, 0, yS1);
|
| 1224 |
+
gOuter.addColorStop(0, `rgba(255,255,255,${r.alpha})`);
|
| 1225 |
+
gOuter.addColorStop(0.55, `rgba(255,255,255,${r.alpha * 0.45})`);
|
| 1226 |
+
gOuter.addColorStop(1, "rgba(255,255,255,0)");
|
| 1227 |
+
fc.fillStyle = gOuter;
|
| 1228 |
+
poly(L, Rr);
|
| 1229 |
+
|
| 1230 |
+
// brighter, narrower inner core riding the same spine (v1 core profile)
|
| 1231 |
+
const LC = [], RC = [];
|
| 1232 |
+
for (let i = 0; i <= RAY_SEGS; i++) {
|
| 1233 |
+
const [lx, ly] = L[i], [rx2, ry2] = Rr[i];
|
| 1234 |
const cxm = (lx + rx2) / 2;
|
| 1235 |
const coreHalf = ((rx2 - lx) / 2) * 0.42; // 42% of the local half-width, as in v1
|
| 1236 |
+
LC.push([cxm - coreHalf, ly]);
|
| 1237 |
+
RC.push([cxm + coreHalf, ry2]);
|
| 1238 |
+
}
|
| 1239 |
+
const gCore = fc.createLinearGradient(0, yS0, 0, yS1);
|
| 1240 |
+
gCore.addColorStop(0, `rgba(255,255,255,${r.alpha * 1.5})`);
|
| 1241 |
+
gCore.addColorStop(0.65, "rgba(255,255,255,0)");
|
| 1242 |
+
fc.fillStyle = gCore;
|
| 1243 |
+
poly(LC, RC);
|
| 1244 |
+
|
| 1245 |
+
const spr = document.createElement("canvas");
|
| 1246 |
+
spr.width = Math.max(1, Math.ceil(bw * S));
|
| 1247 |
+
spr.height = Math.max(1, Math.ceil(bh * S));
|
| 1248 |
+
const sc = spr.getContext("2d");
|
| 1249 |
+
sc.drawImage(full, 0, 0, bw, bh, 0, 0, spr.width, spr.height);
|
| 1250 |
+
|
| 1251 |
+
r.sprite = spr;
|
| 1252 |
+
r.tw = spr.width; r.th = spr.height; // sprite pixel size
|
| 1253 |
+
r.bw = bw; // beam width in CSS px (dest size)
|
| 1254 |
+
r.xMinL = xMinL; // leftmost local x of the beam body
|
| 1255 |
+
if (spr.width > maxTw) maxTw = spr.width;
|
| 1256 |
+
if (spr.height > maxTh) maxTh = spr.height;
|
| 1257 |
+
}
|
| 1258 |
+
|
| 1259 |
+
// shared tint canvas: per frame we recolor each white sprite here with the current
|
| 1260 |
+
// light.ray color, so beams still shift hue through day β dusk β night like before
|
| 1261 |
+
tintCanvas = document.createElement("canvas");
|
| 1262 |
+
tintCanvas.width = Math.max(1, maxTw);
|
| 1263 |
+
tintCanvas.height = Math.max(1, maxTh);
|
| 1264 |
+
tctx = tintCanvas.getContext("2d");
|
| 1265 |
+
|
| 1266 |
+
raySpritesBuiltFor = W + "x" + H;
|
| 1267 |
+
}
|
| 1268 |
+
|
| 1269 |
+
function drawRays(t, light) {
|
| 1270 |
+
if (!tintCanvas || raySpritesBuiltFor !== W + "x" + H) buildRaySprites();
|
| 1271 |
+
const [rr, rg, rb] = light.ray;
|
| 1272 |
+
const tintRGB = `rgb(${rr},${rg},${rb})`;
|
| 1273 |
+
const yTop0 = -10, ySpan = H * 0.95 + 10;
|
| 1274 |
+
|
| 1275 |
+
for (const r of rays) {
|
| 1276 |
+
// 1) recolor the white sprite into the shared temp canvas β one blit + one fill,
|
| 1277 |
+
// GPU-cheap, and it keeps the beams' day/dusk/night hue shifting like before
|
| 1278 |
+
tctx.clearRect(0, 0, r.tw, r.th);
|
| 1279 |
+
tctx.globalCompositeOperation = "source-over";
|
| 1280 |
+
tctx.drawImage(r.sprite, 0, 0);
|
| 1281 |
+
tctx.globalCompositeOperation = "source-in";
|
| 1282 |
+
tctx.fillStyle = tintRGB;
|
| 1283 |
+
tctx.fillRect(0, 0, r.tw, r.th);
|
| 1284 |
+
|
| 1285 |
+
// 2) blit horizontal slices with the undulation offset at each depth β same wave
|
| 1286 |
+
// math as before, so the beams shimmer exactly like they always have
|
| 1287 |
+
const off = Math.sin(t * r.speed * 6 + r.x) * 55; // slow drift of the whole beam
|
| 1288 |
+
for (let i = 0; i < RAY_SLICES; i++) {
|
| 1289 |
+
const vT = i / RAY_SLICES, vB = (i + 1) / RAY_SLICES, vM = (vT + vB) / 2;
|
| 1290 |
+
const ya = yTop0 + vT * ySpan, yb = yTop0 + vB * ySpan;
|
| 1291 |
+
const grow = 0.35 + 0.65 * vM; // straighter up top, bends more with depth
|
| 1292 |
+
const wave =
|
| 1293 |
+
Math.sin(vM * r.uLen + t * r.uSpd + r.uPh) * r.uAmp +
|
| 1294 |
+
Math.sin(vM * r.vLen - t * r.vSpd + r.vPh) * r.vAmp;
|
| 1295 |
+
const dx = r.x + off + wave * grow; // undulating center x at this depth
|
| 1296 |
+
ctx.drawImage(tintCanvas,
|
| 1297 |
+
0, (ya + 10 + RAY_PAD) * RAY_SPRITE_SCALE, r.tw, (yb - ya) * RAY_SPRITE_SCALE,
|
| 1298 |
+
dx + r.xMinL - RAY_PAD, ya, r.bw, yb - ya);
|
| 1299 |
}
|
|
|
|
|
|
|
| 1300 |
}
|
| 1301 |
}
|
| 1302 |
|
|
|
|
| 1695 |
g.putImageData(img, 0, 0);
|
| 1696 |
grainPattern = ctx.createPattern(tile, "repeat"); // cached once, reused every frame
|
| 1697 |
}
|
| 1698 |
+
let grainOx = 0, grainOy = 0, lastGrainTime = 0;
|
| 1699 |
function drawGrain() {
|
| 1700 |
if (!grainPattern) return;
|
| 1701 |
+
// v17: half-speed grain β re-randomize the offset every other frame instead of every frame
|
| 1702 |
+
const now = performance.now();
|
| 1703 |
+
if (now - lastGrainTime >= 33) { // ~30fps grain updates instead of per-frame (~60fps)
|
| 1704 |
+
grainOx = (Math.random() * GRAIN_TILE) | 0;
|
| 1705 |
+
grainOy = (Math.random() * GRAIN_TILE) | 0;
|
| 1706 |
+
lastGrainTime = now;
|
| 1707 |
+
}
|
| 1708 |
+
const ox = grainOx, oy = grainOy;
|
| 1709 |
ctx.save();
|
| 1710 |
ctx.globalCompositeOperation = "overlay";
|
| 1711 |
ctx.globalAlpha = 0.7; // mild: subtle shimmer, not heavy noise
|
|
|
|
| 1718 |
// Precomputed static layers β the sand and rocks never move, so they are
|
| 1719 |
// rendered once to an offscreen canvas and blitted each frame (v6) instead of
|
| 1720 |
// re-drawing their gradients and paths every single frame.
|
| 1721 |
+
// PERF: half-resolution offscreen buffer for the light rays (see drawRays) β the
|
| 1722 |
+
// per-ray blur() filter is far cheaper rasterizing small areas than full-screen.
|
| 1723 |
+
|
| 1724 |
+
|
| 1725 |
let staticLayer = null;
|
| 1726 |
function buildStaticLayer() {
|
| 1727 |
staticLayer = document.createElement("canvas");
|
|
|
|
| 1812 |
|
| 1813 |
function doResize() {
|
| 1814 |
resize();
|
| 1815 |
+
buildRaySprites(); // v23: rebuild the pre-blurred beam sprites for the new size
|
| 1816 |
buildStaticLayer();
|
| 1817 |
if (!grainPattern) buildGrainTile(); // v8: grain tile is size-independent, build once
|
| 1818 |
}
|