Deepgrain Foundry: move from a claude.ai artifact to a local repository
The game as of artifact version 24, ready for local development with coding agents. - Source split from three large files into 52 small ones under src/sim, src/render and src/ui, joined in the order in src/manifest.json by build.py. The joined code is unchanged apart from comments; every headless suite gives the same output, and screenshots in both looks are pixel-identical to before. - Light tidying with no change in behaviour: one movement scan loop, shared hazard-stripe and belt shader helpers, unused code removed, a shared check helper for the test suites. - World files can be exported as an ordinary download outside claude.ai. - Tests: broken old scripts removed, experiments moved to tools/, test/run_all.py added. Tools: shot.py (repeatable screenshots), where.py, map.py. - Docs: AGENTS.md, CLAUDE.md, NOTES.md (the owner's to-do inbox), docs/PROJECT_MAP.md, DESIGN.md, TESTING.md, systems notes, the owner's requests and an agent log. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K9kLmYADKSEjuwUN3rFxDA
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
// Propagated coloured light from glowing and burning cells.
|
||||
// ---------- Lighting ----------
|
||||
// Light is computed on a coarse grid (2 or 4 grains per cell), written into a
|
||||
// small canvas, then the GPU upscales it smoothly and multiplies it over the world.
|
||||
function computeLight() {
|
||||
const res = SET.lighting === 'fast' ? 4 : 2;
|
||||
const M = Math.ceil(28 / res);
|
||||
const lx0 = Math.floor(R.vx0 / res) - M, ly0 = Math.floor(R.vy0 / res) - M;
|
||||
const lw = Math.ceil(R.bw / res) + 2 * M, lh = Math.ceil(R.bh / res) + 2 * M;
|
||||
if (lw !== R.lw || lh !== R.lh || res !== R.lres) {
|
||||
R.lw = lw; R.lh = lh; R.lres = res;
|
||||
R.lr = new Float32Array(lw * lh); R.lg = new Float32Array(lw * lh); R.lb = new Float32Array(lw * lh); R.latt = new Float32Array(lw * lh);
|
||||
R.lightC.width = lw; R.lightC.height = lh;
|
||||
R.lightImg = R.lightX.createImageData(lw, lh);
|
||||
R.lightPx = new Uint32Array(R.lightImg.data.buffer);
|
||||
}
|
||||
R.lx0 = lx0; R.ly0 = ly0;
|
||||
const lr = R.lr, lg = R.lg, lb = R.lb, la = R.latt;
|
||||
// per-grain transmittance: air .978, gas .96, liquid .93, solid .935 (light soaks a little way into rock)
|
||||
const P = res;
|
||||
const aAir = Math.pow(0.978, P), aGas = Math.pow(0.96, P), aLiq = Math.pow(0.93, P), aSol = Math.pow(0.9, P);
|
||||
const skyK = SET.daylight * (1 - 0.32 * (R.rain || 0)); // overcast while it rains
|
||||
for (let ly = 0; ly < lh; ly++) {
|
||||
const wy = (ly0 + ly) * res;
|
||||
for (let lx = 0; lx < lw; lx++) {
|
||||
const wx = (lx0 + lx) * res, k = ly * lw + lx;
|
||||
if (wx < 0 || wy < 0 || wx >= W || wy >= H) { la[k] = 0; lr[k] = lg[k] = lb[k] = 0; continue; }
|
||||
const i = wy * W + wx, m = mat[i], ph = PH[m];
|
||||
la[k] = ph === PH_EMPTY ? (bgm[i] ? aGas : aAir) : ph === PH_GAS ? aGas : ph === PH_LIQUID ? aLiq : m === GLASS || m === ICE ? aGas : aSol;
|
||||
let r = 0, g = 0, b = 0;
|
||||
if (wy < roofY[wx]) { r = skyK; g = skyK * 0.98; b = skyK * 0.94; }
|
||||
else if (wy < surfY[wx] && ph !== PH_SOLID) { const v = skyK * 0.9; r = v; g = v * 0.98; b = v * 0.95; }
|
||||
// emission from heat: hottest grain in the block
|
||||
let T = 0;
|
||||
const xe = Math.min(W, wx + res), ye = Math.min(H, wy + res);
|
||||
for (let yy = wy; yy < ye; yy++) { const row = yy * W; for (let xx = wx; xx < xe; xx++) { const tt = temp[row + xx]; if (tt > T) T = tt; } }
|
||||
if (T > 520) {
|
||||
const gi = glowIndex(T), e = GLOW_W[gi] * 1.35;
|
||||
r += GLOW_R[gi] / 255 * e; g += GLOW_G[gi] / 255 * e; b += GLOW_B[gi] / 255 * e;
|
||||
}
|
||||
lr[k] = r; lg[k] = g; lb[k] = b;
|
||||
}
|
||||
}
|
||||
// bot lamp
|
||||
if (bot.lamp) {
|
||||
const cx = (bot.x + bot.w / 2) / res - lx0, cy = (bot.y + bot.h / 2) / res - ly0, rad = 34 / res;
|
||||
for (let ly = Math.max(0, Math.floor(cy - rad)); ly < Math.min(lh, cy + rad); ly++)
|
||||
for (let lx = Math.max(0, Math.floor(cx - rad)); lx < Math.min(lw, cx + rad); lx++) {
|
||||
const d = Math.hypot(lx - cx, ly - cy) / rad;
|
||||
if (d >= 1) continue;
|
||||
const v = (1 - d) * (1 - d) * 1.05, k = ly * lw + lx;
|
||||
if (lr[k] < v) lr[k] = v; if (lg[k] < v * 0.93) lg[k] = v * 0.93; if (lb[k] < v * 0.8) lb[k] = v * 0.8;
|
||||
}
|
||||
}
|
||||
// propagate: forward sweep (from left/up) then backward sweep (from right/down)
|
||||
for (let ly = 0; ly < lh; ly++) for (let lx = 0; lx < lw; lx++) {
|
||||
const k = ly * lw + lx, a = la[k];
|
||||
let r = lr[k], g = lg[k], b = lb[k];
|
||||
if (lx > 0) { const j = k - 1; if (lr[j] * a > r) r = lr[j] * a; if (lg[j] * a > g) g = lg[j] * a; if (lb[j] * a > b) b = lb[j] * a; }
|
||||
if (ly > 0) { const j = k - lw; if (lr[j] * a > r) r = lr[j] * a; if (lg[j] * a > g) g = lg[j] * a; if (lb[j] * a > b) b = lb[j] * a; }
|
||||
lr[k] = r; lg[k] = g; lb[k] = b;
|
||||
}
|
||||
for (let ly = lh - 1; ly >= 0; ly--) for (let lx = lw - 1; lx >= 0; lx--) {
|
||||
const k = ly * lw + lx, a = la[k];
|
||||
let r = lr[k], g = lg[k], b = lb[k];
|
||||
if (lx < lw - 1) { const j = k + 1; if (lr[j] * a > r) r = lr[j] * a; if (lg[j] * a > g) g = lg[j] * a; if (lb[j] * a > b) b = lb[j] * a; }
|
||||
if (ly < lh - 1) { const j = k + lw; if (lr[j] * a > r) r = lr[j] * a; if (lg[j] * a > g) g = lg[j] * a; if (lb[j] * a > b) b = lb[j] * a; }
|
||||
lr[k] = r; lg[k] = g; lb[k] = b;
|
||||
}
|
||||
// write the light canvas
|
||||
const fl = SET.ambient, px = R.lightPx, n = lw * lh;
|
||||
for (let k = 0; k < n; k++) {
|
||||
let r = lr[k], g = lg[k], b = lb[k];
|
||||
if (r < fl) r = fl; if (g < fl) g = fl; if (b < fl) b = fl;
|
||||
r = r >= 1 ? 255 : (r * 255) | 0; g = g >= 1 ? 255 : (g * 255) | 0; b = b >= 1 ? 255 : (b * 255) | 0;
|
||||
px[k] = 0xff000000 | (b << 16) | (g << 8) | r;
|
||||
}
|
||||
if (!R.smoothOn) R.lightX.putImageData(R.lightImg, 0, 0);
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
// Machine overlays: fans, engines, refiner, cutter, silo and the rest, drawn on top of the world.
|
||||
// ---------- Overlays ----------
|
||||
function cellRect(x, y, w, h) { const p = worldToScreenPx(x, y); return [p.x, p.y, w * R.zp, h * R.zp]; }
|
||||
// Yellow and black hazard stripes on both top corners of a hopper
|
||||
function drawHazardRims(ctx, sx, sy, sw, zp) {
|
||||
for (let k = 0; k < 2; k++) {
|
||||
const rx = k ? sx + sw - 2 * zp : sx;
|
||||
for (let j = 0; j < 4; j++) { ctx.fillStyle = j & 1 ? '#1d1b18' : '#e0b43c'; ctx.fillRect(rx, sy + j * zp * 0.5, 2 * zp, zp * 0.5); }
|
||||
}
|
||||
}
|
||||
function drawMachines() {
|
||||
const ctx = R.ctx, zp = R.zp;
|
||||
for (const mm of machines) {
|
||||
if (!mm) continue;
|
||||
const [sx, sy, sw, sh] = cellRect(mm.x, mm.y, mm.w, mm.h);
|
||||
if (sx > R.canvas.width || sy > R.canvas.height || sx + sw < 0 || sy + sh < 0) continue;
|
||||
ctx.save();
|
||||
if (mm.type === 'dock') {
|
||||
// hazard stripes along the hopper rim and a sign on a post
|
||||
drawHazardRims(ctx, sx, sy, sw, zp);
|
||||
// the grate in its floor
|
||||
ctx.fillStyle = 'rgba(30,28,26,0.85)'; ctx.fillRect(sx + 2 * zp, sy + 13 * zp, 20 * zp, zp);
|
||||
ctx.fillStyle = 'rgba(150,160,170,0.8)';
|
||||
for (let k = 0; k < 20; k += 2) ctx.fillRect(sx + (2 + k) * zp, sy + 13 * zp, zp * 0.5, zp);
|
||||
if (!R.monoFont) R.monoFont = getComputedStyle(document.documentElement).getPropertyValue('--font-mono').trim() || 'monospace';
|
||||
const fs = Math.max(9, Math.round(zp * 2.6));
|
||||
ctx.font = `700 ${fs}px ${R.monoFont}`;
|
||||
const label = 'TRADE DOCK', tw = ctx.measureText(label).width + fs;
|
||||
const px = sx + zp * 0.5, py = sy - zp * 7;
|
||||
ctx.fillStyle = '#4a4036'; ctx.fillRect(px + zp * 0.5, py + fs * 1.4, zp * 0.8, sy - py - fs * 1.4);
|
||||
ctx.fillStyle = '#2b4a3f'; ctx.fillRect(px, py, tw, fs * 1.5);
|
||||
ctx.strokeStyle = '#e7dccb'; ctx.lineWidth = Math.max(1, zp * 0.2); ctx.strokeRect(px + 1, py + 1, tw - 2, fs * 1.5 - 2);
|
||||
ctx.fillStyle = '#efe6d8'; ctx.textBaseline = 'middle'; ctx.fillText(label, px + fs * 0.5, py + fs * 0.78);
|
||||
} else if (mm.type === 'wheel') {
|
||||
const cx = sx + sw / 2, cy = sy + sh / 2, rad = sw / 2 - zp * 0.3;
|
||||
ctx.strokeStyle = '#8a6a44'; ctx.lineWidth = Math.max(1, zp * 0.7);
|
||||
ctx.beginPath(); ctx.arc(cx, cy, rad, 0, Math.PI * 2); ctx.stroke();
|
||||
ctx.lineWidth = Math.max(1, zp * 0.5);
|
||||
for (let k = 0; k < 8; k++) {
|
||||
const a = mm.angle + k * Math.PI / 4;
|
||||
ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(cx + Math.cos(a) * rad, cy + Math.sin(a) * rad); ctx.stroke();
|
||||
ctx.fillStyle = '#6e5232';
|
||||
ctx.fillRect(cx + Math.cos(a) * rad - zp * 0.6, cy + Math.sin(a) * rad - zp * 0.6, zp * 1.2, zp * 1.2);
|
||||
}
|
||||
ctx.fillStyle = '#4a3a28'; ctx.beginPath(); ctx.arc(cx, cy, zp * 1.1, 0, Math.PI * 2); ctx.fill();
|
||||
} else if (mm.type === 'fan') {
|
||||
const cx = sx + sw / 2, cy = sy + sh / 2;
|
||||
const net = nets[mm.net], sat = net ? net.sat : 0;
|
||||
ctx.fillStyle = 'rgba(20,22,26,0.55)'; ctx.beginPath(); ctx.arc(cx, cy, sw * 0.38, 0, Math.PI * 2); ctx.fill();
|
||||
ctx.fillStyle = mm.on ? (sat > 0.05 ? '#b9c4cc' : '#7d868e') : '#5a5f66';
|
||||
for (let k = 0; k < 3; k++) {
|
||||
const a = mm.angle + k * Math.PI * 2 / 3;
|
||||
ctx.beginPath(); ctx.ellipse(cx + Math.cos(a) * sw * 0.18, cy + Math.sin(a) * sw * 0.18, sw * 0.17, sw * 0.07, a, 0, Math.PI * 2); ctx.fill();
|
||||
}
|
||||
// direction arrow
|
||||
const dx = [1, 0, -1, 0][mm.dir], dy = [0, 1, 0, -1][mm.dir];
|
||||
ctx.strokeStyle = sat > 0.05 && mm.on ? 'rgba(160,220,255,0.7)' : 'rgba(160,170,180,0.35)'; ctx.lineWidth = Math.max(1, zp * 0.35);
|
||||
ctx.beginPath(); ctx.moveTo(cx + dx * sw * 0.55, cy + dy * sh * 0.55); ctx.lineTo(cx + dx * sw * 0.9, cy + dy * sh * 0.9); ctx.stroke();
|
||||
} else if (mm.type === 'silo') {
|
||||
drawSilo(ctx, mm, sx, sy, zp);
|
||||
} else if (mm.type === 'cutter') {
|
||||
// hazard rims on the hopper, and a toothed saw blade in the floor that spins while it cuts
|
||||
const busy = tick - (mm.busyT || -99) < 12;
|
||||
drawHazardRims(ctx, sx, sy, sw, zp);
|
||||
const cx = sx + 4 * zp, cy = sy + 6.5 * zp, rr = zp * 1.4, a0 = busy ? R.frame * 0.35 : 0;
|
||||
ctx.fillStyle = '#c9cfd4'; ctx.beginPath();
|
||||
for (let t = 0; t < 16; t++) { const a = a0 + t * Math.PI / 8, r2 = t & 1 ? rr : rr * 0.78; ctx.lineTo(cx + Math.cos(a) * r2, cy + Math.sin(a) * r2); }
|
||||
ctx.closePath(); ctx.fill();
|
||||
ctx.fillStyle = '#3a4048'; ctx.beginPath(); ctx.arc(cx, cy, rr * 0.3, 0, Math.PI * 2); ctx.fill();
|
||||
ctx.fillStyle = mm.blocked ? '#c4553f' : busy ? '#e0b43c' : '#4a5058';
|
||||
ctx.fillRect(sx + 2 * zp, sy + 7.6 * zp, 4 * zp, zp * 0.4);
|
||||
} else if (mm.type === 'refiner') {
|
||||
// hopper rim, two toothed grinding rollers in the floor (they turn while it works), the chute
|
||||
const busy = tick - (mm.busyT || -99) < 12;
|
||||
drawHazardRims(ctx, sx, sy, sw, zp);
|
||||
for (let k = 0; k < 2; k++) {
|
||||
const cx = sx + (k ? 7.5 : 4.5) * zp, cy = sy + 11 * zp, rr = zp * 1.15, a0 = (busy ? R.frame * 0.25 : 0) * (k ? -1 : 1);
|
||||
ctx.fillStyle = '#2c3036'; ctx.beginPath(); ctx.arc(cx, cy, rr, 0, Math.PI * 2); ctx.fill();
|
||||
ctx.strokeStyle = '#9aa4ae'; ctx.lineWidth = Math.max(1, zp * 0.3);
|
||||
ctx.beginPath(); for (let t = 0; t < 6; t++) { const a = a0 + t * Math.PI / 3; ctx.moveTo(cx + Math.cos(a) * rr * 0.35, cy + Math.sin(a) * rr * 0.35); ctx.lineTo(cx + Math.cos(a) * rr, cy + Math.sin(a) * rr); } ctx.stroke();
|
||||
}
|
||||
ctx.fillStyle = mm.blocked ? '#c4553f' : busy ? '#e0b43c' : '#4a5058';
|
||||
ctx.beginPath(); ctx.moveTo(sx + 4 * zp, sy + 12 * zp); ctx.lineTo(sx + 8 * zp, sy + 12 * zp); ctx.lineTo(sx + 7 * zp, sy + 12.8 * zp); ctx.lineTo(sx + 5 * zp, sy + 12.8 * zp); ctx.closePath(); ctx.fill();
|
||||
} else if (mm.type === 'engine') {
|
||||
ctx.fillStyle = 'rgba(16,18,22,0.4)'; ctx.fillRect(sx + zp, sy + zp, sw - 2 * zp, sh - 3 * zp);
|
||||
const fx = sx + sw * (mm.dir ? 0.3 : 0.7), fy = sy + sh * 0.42, fr = sh * 0.3;
|
||||
ctx.strokeStyle = '#c3a06a'; ctx.lineWidth = Math.max(1, zp * 0.5);
|
||||
ctx.beginPath(); ctx.arc(fx, fy, fr, 0, Math.PI * 2); ctx.stroke();
|
||||
const ax = fx + Math.cos(mm.angle) * fr * 0.8, ay = fy + Math.sin(mm.angle) * fr * 0.8;
|
||||
ctx.beginPath(); ctx.moveTo(ax, ay); ctx.lineTo(sx + sw * (mm.dir ? 0.75 : 0.25), fy); ctx.stroke();
|
||||
ctx.fillStyle = '#d6b27a'; ctx.fillRect(sx + sw * (mm.dir ? 0.75 : 0.25) - zp, fy - zp * 0.8, zp * 2, zp * 1.6);
|
||||
if (mm.out > 0.3) { ctx.fillStyle = 'rgba(230,236,244,0.5)'; ctx.fillRect(mm.dir ? sx - zp * 1.5 : sx + sw + zp * 0.5, sy + sh - zp * (1 + (R.frame >> 3) % 2), zp, zp); }
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
// The silo: legs and braces, a cone to a slide gate, a ribbed steel drum with a ladder and a sight
|
||||
// glass that shows how full it is, a roof with the loading hatch. All in grain units from its corner.
|
||||
function drawSilo(ctx, mm, sx, sy, zp) {
|
||||
const X = u => sx + u * zp, Y = u => sy + u * zp, full = Math.min(1, (mm.sn || 0) / SILO_CAP);
|
||||
const steel = (x0, x1) => { const g = ctx.createLinearGradient(X(x0), 0, X(x1), 0); g.addColorStop(0, '#3b4249'); g.addColorStop(0.22, '#a9b3bb'); g.addColorStop(0.5, '#87919a'); g.addColorStop(1, '#353c43'); return g; };
|
||||
ctx.save();
|
||||
// legs, with short struts to the cone
|
||||
ctx.fillStyle = '#2f353b';
|
||||
ctx.fillRect(X(0.15), Y(19.5), zp * 0.75, zp * 4.5); ctx.fillRect(X(11.1), Y(19.5), zp * 0.75, zp * 4.5);
|
||||
ctx.strokeStyle = '#2f353b'; ctx.lineWidth = Math.max(1, zp * 0.3);
|
||||
ctx.beginPath(); ctx.moveTo(X(0.8), Y(23)); ctx.lineTo(X(2.6), Y(21)); ctx.moveTo(X(11.2), Y(23)); ctx.lineTo(X(9.4), Y(21)); ctx.stroke();
|
||||
// the cone down to the gate
|
||||
ctx.fillStyle = steel(1, 11); ctx.beginPath(); ctx.moveTo(X(0.8), Y(19.8)); ctx.lineTo(X(11.2), Y(19.8)); ctx.lineTo(X(8.2), Y(23)); ctx.lineTo(X(3.8), Y(23)); ctx.closePath(); ctx.fill();
|
||||
ctx.strokeStyle = 'rgba(0,0,0,0.35)'; ctx.lineWidth = Math.max(1, zp * 0.12); ctx.stroke();
|
||||
// the slide gate: across the opening when shut, pulled aside when open
|
||||
ctx.fillStyle = '#1c1f23'; ctx.fillRect(X(4), Y(23), zp * 4, zp);
|
||||
ctx.fillStyle = mm.open ? '#c99a3c' : '#6b737b';
|
||||
if (mm.open) ctx.fillRect(X(7.6), Y(23.15), zp * 2.4, zp * 0.6); else ctx.fillRect(X(3.6), Y(23.15), zp * 4.8, zp * 0.6);
|
||||
// the drum
|
||||
ctx.fillStyle = steel(0.5, 11.5); ctx.fillRect(X(0.5), Y(1.8), zp * 11, zp * 18.2);
|
||||
ctx.strokeStyle = 'rgba(0,0,0,0.28)'; ctx.lineWidth = Math.max(1, zp * 0.14);
|
||||
for (let k = 1; k < 4; k++) { const yy = Y(1.8 + k * 4.55); ctx.beginPath(); ctx.moveTo(X(0.5), yy); ctx.lineTo(X(11.5), yy); ctx.stroke(); }
|
||||
ctx.strokeStyle = 'rgba(0,0,0,0.12)';
|
||||
for (let k = 1; k < 6; k++) { const xx = X(0.5 + k * 11 / 6); ctx.beginPath(); ctx.moveTo(xx, Y(1.8)); ctx.lineTo(xx, Y(20)); ctx.stroke(); }
|
||||
if (zp >= 4) { ctx.fillStyle = 'rgba(255,255,255,0.35)'; for (let k = 1; k < 4; k++) for (let j = 0; j < 8; j++) { ctx.beginPath(); ctx.arc(X(1 + j * 1.43), Y(1.8 + k * 4.55) - zp * 0.25, Math.max(0.6, zp * 0.09), 0, Math.PI * 2); ctx.fill(); } }
|
||||
// a ladder up the left side
|
||||
ctx.strokeStyle = '#2b3036'; ctx.lineWidth = Math.max(1, zp * 0.14);
|
||||
ctx.beginPath(); ctx.moveTo(X(1.3), Y(2)); ctx.lineTo(X(1.3), Y(19.8)); ctx.moveTo(X(2.3), Y(2)); ctx.lineTo(X(2.3), Y(19.8));
|
||||
for (let yy = 2.6; yy < 19.8; yy += 1.1) { ctx.moveTo(X(1.3), Y(yy)); ctx.lineTo(X(2.3), Y(yy)); }
|
||||
ctx.stroke();
|
||||
// the sight glass: fills from the bottom in the colour of what is inside
|
||||
const gx = 9.1, gy0 = 3, gy1 = 18.8, gh = gy1 - gy0;
|
||||
ctx.fillStyle = '#14181c'; ctx.fillRect(X(gx), Y(gy0), zp * 1.1, zp * gh);
|
||||
if (mm.sn > 0) { ctx.fillStyle = `rgb(${COLR[mm.sm]},${COLG[mm.sm]},${COLB[mm.sm]})`; ctx.fillRect(X(gx + 0.15), Y(gy1 - gh * full), zp * 0.8, zp * gh * full); }
|
||||
ctx.strokeStyle = '#c9d1d8'; ctx.lineWidth = Math.max(1, zp * 0.1);
|
||||
ctx.strokeRect(X(gx), Y(gy0), zp * 1.1, zp * gh);
|
||||
for (let k = 1; k < 4; k++) { const yy = Y(gy1 - gh * k / 4); ctx.beginPath(); ctx.moveTo(X(gx - 0.35), yy); ctx.lineTo(X(gx), yy); ctx.stroke(); }
|
||||
// what it holds, stencilled on the drum
|
||||
if (zp >= 3) {
|
||||
if (!R.monoFont) R.monoFont = getComputedStyle(document.documentElement).getPropertyValue('--font-mono').trim() || 'monospace';
|
||||
const fs = Math.max(8, Math.round(zp * 1.15));
|
||||
ctx.font = `700 ${fs}px ${R.monoFont}`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
||||
ctx.fillStyle = 'rgba(20,24,28,0.75)';
|
||||
ctx.fillText('SILO', X(5.6), Y(5));
|
||||
ctx.font = `600 ${Math.max(8, Math.round(zp * 0.95))}px ${R.monoFont}`;
|
||||
ctx.fillText(mm.sn ? mm.sn.toLocaleString('en-US') : 'EMPTY', X(5.6), Y(16.8));
|
||||
if (mm.sn) { ctx.fillStyle = `rgb(${COLR[mm.sm]},${COLG[mm.sm]},${COLB[mm.sm]})`; ctx.fillRect(X(4.4), Y(17.9), zp * 2.4, zp * 0.55); }
|
||||
}
|
||||
// roof with a rim, and the loading hatch (its lid stands open)
|
||||
ctx.fillStyle = '#5b646c'; ctx.fillRect(X(0.2), Y(1.2), zp * 11.6, zp * 0.8);
|
||||
ctx.fillStyle = '#7d878f'; ctx.fillRect(X(0.6), Y(0.6), zp * 2.4, zp * 0.7); ctx.fillRect(X(9), Y(0.6), zp * 2.4, zp * 0.7);
|
||||
ctx.fillStyle = '#15181b'; ctx.fillRect(X(3), Y(0.9), zp * 6, zp * 1.1);
|
||||
ctx.strokeStyle = tick - (mm.fillT || -99) < 10 ? '#e0b43c' : '#9aa4ae'; ctx.lineWidth = Math.max(1, zp * 0.3);
|
||||
ctx.beginPath(); ctx.moveTo(X(8.9), Y(0.9)); ctx.lineTo(X(10.2), Y(-1.4)); ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
// Other overlays: floating text, rain and snow, flow arrows, filter tags, molds, the bot, particles.
|
||||
function spawnFloater(x, y, text) { R.floaters = R.floaters || []; if (R.floaters.length < 30) R.floaters.push({ x, y, text, life: 70 }); }
|
||||
// Rain as thin falling streaks, snow as soft flakes
|
||||
function drawRain() {
|
||||
if (!flyers.length) return;
|
||||
const ctx = R.ctx, zp = R.zp, v = viewCells();
|
||||
const x0 = R.camX - 2, y0 = R.camY - 4, x1 = R.camX + v.w + 2, y1 = R.camY + v.h + 2;
|
||||
ctx.save();
|
||||
ctx.lineCap = 'round';
|
||||
ctx.strokeStyle = 'rgba(176,204,236,0.5)'; ctx.lineWidth = Math.max(1, zp * 0.22);
|
||||
ctx.beginPath();
|
||||
let snow = false;
|
||||
for (const f of flyers) {
|
||||
if (!f.rain || f.x < x0 || f.x > x1 || f.y < y0 || f.y > y1) continue;
|
||||
if (f.m !== WATER) { snow = true; continue; }
|
||||
const a = worldToScreenPx(f.x, f.y), len = Math.max(1.5, Math.hypot(f.vx, f.vy) * 3.2);
|
||||
ctx.moveTo(a.x, a.y); ctx.lineTo(a.x - f.vx / Math.max(0.2, f.vy) * len * zp * 0.5, a.y - len * zp);
|
||||
}
|
||||
ctx.stroke();
|
||||
if (snow) {
|
||||
ctx.fillStyle = 'rgba(244,247,252,0.9)';
|
||||
for (const f of flyers) {
|
||||
if (!f.rain || f.m === WATER || f.x < x0 || f.x > x1 || f.y < y0 || f.y > y1) continue;
|
||||
const a = worldToScreenPx(f.x + Math.sin(f.age * 0.07 + f.x) * 0.6, f.y), r = Math.max(1, zp * 0.42);
|
||||
ctx.beginPath(); ctx.arc(a.x, a.y, r, 0, Math.PI * 2); ctx.fill();
|
||||
}
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
// Info overlay: a small arrow on every belt and screw tube block on screen, the way it moves things
|
||||
function drawFlowArrows() {
|
||||
const ctx = R.ctx, zp = R.zp, v = viewCells();
|
||||
const bx0 = Math.max(0, Math.floor(R.camX / BS)), bx1 = Math.min(BW - 1, Math.floor((R.camX + v.w) / BS));
|
||||
const by0 = Math.max(0, Math.floor(R.camY / BS)), by1 = Math.min(BH - 1, Math.floor((R.camY + v.h) / BS));
|
||||
const s = BS * zp, r = Math.max(3, s * 0.22);
|
||||
ctx.save();
|
||||
ctx.lineCap = 'round'; ctx.lineJoin = 'round';
|
||||
for (let by = by0; by <= by1; by++) for (let bx = bx0; bx <= bx1; bx++) {
|
||||
const b = by * BW + bx, code = blockP[b];
|
||||
if (code !== BP_BELT && code !== BP_TUBE && code !== BP_PUMP && code !== BP_OUTLET) continue;
|
||||
const vv = blockV[b];
|
||||
const [dx, dy] = code === BP_BELT ? (vv ? [1, 0] : [-1, 0]) : code === BP_PUMP ? PUMP_V[vv & 3] : code === BP_OUTLET ? OUTLET_V[vv & 3] : [[0, -1], [1, 0], [0, 1], [-1, 0]][vv & 3];
|
||||
const c = worldToScreenPx(bx * BS + BS / 2, by * BS + BS / 2);
|
||||
ctx.fillStyle = 'rgba(14,12,11,0.55)'; ctx.beginPath(); ctx.arc(c.x, c.y, r * 1.35, 0, Math.PI * 2); ctx.fill();
|
||||
ctx.strokeStyle = code === BP_TUBE ? '#ffc58a' : '#e7dccb'; ctx.lineWidth = Math.max(1.2, r * 0.32);
|
||||
// a chevron pointing (dx, dy)
|
||||
const px = -dy, py = dx;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(c.x - dx * r * 0.45 + px * r * 0.7, c.y - dy * r * 0.45 + py * r * 0.7);
|
||||
ctx.lineTo(c.x + dx * r * 0.55, c.y + dy * r * 0.55);
|
||||
ctx.lineTo(c.x - dx * r * 0.45 - px * r * 0.7, c.y - dy * r * 0.45 - py * r * 0.7);
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
// Outline a run of blocks: strong while it is being edited, faint when pointed at
|
||||
function drawRunOutline(run, strong) {
|
||||
if (!run || !run.length) return;
|
||||
const ctx = R.ctx, zp = R.zp;
|
||||
const x0 = (run[0] % BW) * BS, x1 = (run[run.length - 1] % BW + 1) * BS, y = ((run[0] / BW) | 0) * BS;
|
||||
const a = worldToScreenPx(x0, y), b = worldToScreenPx(x1, y + BS);
|
||||
ctx.save();
|
||||
ctx.strokeStyle = strong ? 'rgba(255,190,90,0.95)' : 'rgba(236,226,210,0.45)'; ctx.lineWidth = Math.max(1, zp * (strong ? 0.5 : 0.25));
|
||||
if (!strong) ctx.setLineDash([zp, zp * 0.7]);
|
||||
ctx.strokeRect(a.x - zp * 0.3, a.y - zp * 0.3, b.x - a.x + zp * 0.6, b.y - a.y + zp * 0.6);
|
||||
ctx.restore();
|
||||
}
|
||||
// Tags over filters: what falls through (↓) and what rides along, and which way.
|
||||
// showAll tags every run of matching filters on screen; otherwise only the run under hoverB.
|
||||
function drawFilterTags(showAll, hoverB) {
|
||||
if (!filterCfg.size || (!showAll && hoverB < 0)) return;
|
||||
const ctx = R.ctx, u = Math.max(1, R.canvas.width / window.innerWidth);
|
||||
const v = viewCells();
|
||||
const bx0 = Math.max(0, Math.floor(R.camX / BS) - 1), bx1 = Math.min(BW - 1, Math.floor((R.camX + v.w) / BS) + 1);
|
||||
const by0 = Math.max(0, Math.floor(R.camY / BS) - 1), by1 = Math.min(BH - 1, Math.floor((R.camY + v.h) / BS) + 1);
|
||||
if (!R.monoFont) R.monoFont = getComputedStyle(document.documentElement).getPropertyValue('--font-mono').trim() || 'monospace';
|
||||
ctx.save();
|
||||
ctx.font = `600 ${Math.round(11 * u)}px ${R.monoFont}`; ctx.textBaseline = 'middle';
|
||||
const sw = Math.round(9 * u), gap = Math.round(3 * u), pad = Math.round(5 * u), th = Math.round(17 * u);
|
||||
const runs = showAll ? [] : [filterRun(hoverB)];
|
||||
if (showAll) for (let by = by0; by <= by1; by++) for (let bx = bx0; bx <= bx1; bx++) {
|
||||
const b = by * BW + bx;
|
||||
if (blockP[b] !== BP_FILTER) continue;
|
||||
const run = filterRun(b);
|
||||
if (run[0] === b || bx === bx0) runs.push(run);
|
||||
bx = Math.max(bx, (run[run.length - 1] % BW));
|
||||
}
|
||||
for (const run of runs) {
|
||||
if (!run.length) continue;
|
||||
const b = run[0], c = filterCfg.get(b), ms = c ? c.m : [], inv = c ? c.inv : 0, right = blockV[b] & 128;
|
||||
// segment contents: swatches, or a word when it is "the rest"
|
||||
const segs = [
|
||||
{ icon: '↓', col: '#7fd6c2', mats: inv ? null : ms, word: inv ? (ms.length ? 'rest' : 'all') : 'none' },
|
||||
{ icon: right ? '→' : '←', col: '#ffd27a', mats: inv ? ms : null, word: inv ? 'none' : (ms.length ? 'rest' : 'all') },
|
||||
];
|
||||
let w = pad;
|
||||
for (const sg of segs) {
|
||||
sg.iw = ctx.measureText(sg.icon).width;
|
||||
sg.show = sg.mats && sg.mats.length ? sg.mats.slice(0, 5) : null;
|
||||
sg.more = sg.mats && sg.mats.length > 5 ? `+${sg.mats.length - 5}` : '';
|
||||
sg.cw = sg.show ? sg.show.length * (sw + 2) - 2 + (sg.more ? gap + ctx.measureText(sg.more).width : 0) : ctx.measureText(sg.word).width;
|
||||
w += sg.iw + gap + sg.cw + pad;
|
||||
}
|
||||
w += gap;
|
||||
const x0 = (run[0] % BW) * BS, x1 = (run[run.length - 1] % BW + 1) * BS, y = ((b / BW) | 0) * BS;
|
||||
const p = worldToScreenPx((x0 + x1) / 2, y);
|
||||
let tx = Math.round(p.x - w / 2), ty = Math.round(p.y - th - 4 * u);
|
||||
if (tx + w < 0 || tx > R.canvas.width || ty + th < 0 || ty > R.canvas.height) continue;
|
||||
ctx.fillStyle = 'rgba(14,12,11,0.86)'; ctx.fillRect(tx, ty, w, th);
|
||||
ctx.strokeStyle = 'rgba(236,226,210,0.22)'; ctx.lineWidth = u; ctx.strokeRect(tx + u / 2, ty + u / 2, w - u, th - u);
|
||||
let cx = tx + pad;
|
||||
segs.forEach((sg, k) => {
|
||||
if (k) { ctx.fillStyle = 'rgba(236,226,210,0.25)'; ctx.fillRect(cx - pad / 2 - gap / 2, ty + 3 * u, u, th - 6 * u); cx += gap; }
|
||||
ctx.fillStyle = sg.col; ctx.fillText(sg.icon, cx, ty + th / 2 + u * 0.5); cx += sg.iw + gap;
|
||||
if (sg.show) {
|
||||
for (const m of sg.show) { ctx.fillStyle = `rgb(${COLR[m]},${COLG[m]},${COLB[m]})`; ctx.fillRect(cx, ty + (th - sw) / 2, sw, sw); ctx.strokeStyle = 'rgba(0,0,0,0.55)'; ctx.strokeRect(cx + 0.5, ty + (th - sw) / 2 + 0.5, sw - 1, sw - 1); cx += sw + 2; }
|
||||
cx -= 2;
|
||||
if (sg.more) { cx += gap; ctx.fillStyle = '#e7dccb'; ctx.fillText(sg.more, cx, ty + th / 2 + u * 0.5); cx += ctx.measureText(sg.more).width; }
|
||||
} else { ctx.fillStyle = 'rgba(231,220,203,0.85)'; ctx.fillText(sg.word, cx, ty + th / 2 + u * 0.5); cx += sg.cw; }
|
||||
cx += pad;
|
||||
});
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
function drawFloaters() {
|
||||
const fl = R.floaters; if (!fl || !fl.length) return;
|
||||
const ctx = R.ctx, zp = R.zp;
|
||||
if (!R.monoFont) R.monoFont = getComputedStyle(document.documentElement).getPropertyValue('--font-mono').trim() || 'monospace';
|
||||
ctx.save();
|
||||
ctx.font = `700 ${Math.max(11, Math.round(zp * 3))}px ${R.monoFont}`; ctx.textAlign = 'center'; ctx.textBaseline = 'bottom';
|
||||
for (let k = fl.length - 1; k >= 0; k--) {
|
||||
const f = fl[k]; f.life--; f.y -= 0.12;
|
||||
if (f.life <= 0) { fl.splice(k, 1); continue; }
|
||||
const s = worldToScreenPx(f.x, f.y), a = Math.min(1, f.life / 25);
|
||||
ctx.fillStyle = `rgba(14,12,11,${0.6 * a})`; ctx.fillText(f.text, s.x + 1, s.y + 1);
|
||||
ctx.fillStyle = `rgba(255,210,122,${a})`; ctx.fillText(f.text, s.x, s.y);
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
function drawMolds(hoverMold) {
|
||||
const ctx = R.ctx, zp = R.zp;
|
||||
ctx.save();
|
||||
if (!R.monoFont) R.monoFont = getComputedStyle(document.documentElement).getPropertyValue('--font-mono').trim() || 'monospace';
|
||||
ctx.font = `600 ${Math.max(10, Math.round(zp * 3.4))}px ${R.monoFont}`;
|
||||
ctx.textAlign = 'center'; ctx.textBaseline = 'bottom';
|
||||
for (const mo of molds) {
|
||||
const [sx, sy, sw, sh] = cellRect(mo.x, mo.y, mo.w, mo.h);
|
||||
if (sx > R.canvas.width || sy > R.canvas.height || sx + sw < 0 || sy + sh < 0) continue;
|
||||
const ready = mo.state === 'ready', flawed = mo.state === 'flawed';
|
||||
ctx.setLineDash([zp, zp]);
|
||||
ctx.strokeStyle = ready ? 'rgba(255,200,90,0.95)' : flawed ? 'rgba(230,90,70,0.8)' : 'rgba(200,190,170,0.4)';
|
||||
ctx.lineWidth = Math.max(1, zp * 0.3);
|
||||
ctx.strokeRect(sx - zp * 0.5, sy - zp * 0.5, sw + zp, sh + zp);
|
||||
ctx.setLineDash([]);
|
||||
if (ready || flawed || mo === hoverMold || mo.state === 'filling' || mo.state === 'short') {
|
||||
const label = ready ? `${mo.kind === 'ingot' ? (mo.ready.length === 1 ? 'Bar' : `${mo.ready.length} bars`) : mo.kind === 'bit' ? 'Bit' : 'Tank'} set · F` : flawed ? 'Flawed casting' : mo.state === 'short' ? 'Needs more metal' : mo.state === 'filling' ? `Filling ${Math.round(mo.fill * 100)}%` : MOLDS[mo.kind].name;
|
||||
ctx.fillStyle = 'rgba(14,12,11,0.75)';
|
||||
const tw = ctx.measureText(label).width + zp * 2;
|
||||
ctx.fillRect(sx + sw / 2 - tw / 2, sy - zp * 5.2, tw, zp * 4.2);
|
||||
ctx.fillStyle = ready ? '#ffcf7a' : flawed ? '#ff8a72' : '#e7dccb';
|
||||
ctx.fillText(label, sx + sw / 2, sy - zp * 1.4);
|
||||
}
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
function drawBot() {
|
||||
const ctx = R.ctx, zp = R.zp;
|
||||
const [sx, sy, sw, sh] = cellRect(bot.x, bot.y, bot.w, bot.h);
|
||||
const cx = sx + sw / 2, cy = sy + sh / 2;
|
||||
const bob = Math.sin(R.frame * 0.12) * zp * 0.25;
|
||||
ctx.save();
|
||||
// thruster glow
|
||||
if (bot.thrust > 0.1) {
|
||||
const fl = (0.6 + Math.random() * 0.4) * bot.thrust;
|
||||
const grd = ctx.createRadialGradient(cx, sy + sh + bob, 0, cx, sy + sh + bob, zp * 3.2 * fl);
|
||||
grd.addColorStop(0, 'rgba(255,214,140,0.9)'); grd.addColorStop(1, 'rgba(255,120,40,0)');
|
||||
ctx.fillStyle = grd; ctx.fillRect(cx - zp * 4, sy + sh - zp + bob, zp * 8, zp * 5);
|
||||
}
|
||||
// body
|
||||
ctx.fillStyle = '#c8bfb2';
|
||||
ctx.beginPath(); ctx.roundRect(sx + zp * 0.2, sy + zp * 0.6 + bob, sw - zp * 0.4, sh - zp * 1.4, zp * 1.4); ctx.fill();
|
||||
ctx.fillStyle = '#8c8378';
|
||||
ctx.fillRect(sx + zp * 0.2, sy + sh - zp * 1.6 + bob, sw - zp * 0.4, zp * 0.8);
|
||||
// visor
|
||||
ctx.fillStyle = '#1b1715';
|
||||
const vx = cx + bot.face * zp * 0.5;
|
||||
ctx.beginPath(); ctx.roundRect(vx - zp * 1.5, sy + zp * 1.4 + bob, zp * 3, zp * 1.6, zp * 0.6); ctx.fill();
|
||||
ctx.fillStyle = bot.lamp ? '#ffb45a' : '#6b5a48';
|
||||
ctx.fillRect(vx + bot.face * zp * 0.5 - zp * 0.45, sy + zp * 1.8 + bob, zp * 0.9, zp * 0.8);
|
||||
// antenna
|
||||
ctx.strokeStyle = '#8c8378'; ctx.lineWidth = Math.max(1, zp * 0.3);
|
||||
ctx.beginPath(); ctx.moveTo(cx - bot.face * zp, sy + zp * 0.6 + bob); ctx.lineTo(cx - bot.face * zp * 1.6, sy - zp * 0.6 + bob); ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
function spawnDebris(x, y, m) {
|
||||
if (!SET.particles || R.particles.length > 400) return;
|
||||
for (let k = 0; k < 2; k++) R.particles.push({ x: x + Math.random(), y: y + Math.random(), vx: (Math.random() - 0.5) * 0.5, vy: -Math.random() * 0.5, life: 20 + Math.random() * 20, r: COLR[m], g: COLG[m], b: COLB[m] });
|
||||
}
|
||||
function spawnSpark(x, y) {
|
||||
if (!SET.particles || R.particles.length > 400) return;
|
||||
R.particles.push({ x, y, vx: (Math.random() - 0.5) * 0.9, vy: -Math.random() * 0.8, life: 10 + Math.random() * 12, r: 255, g: 190 + Math.random() * 60, b: 90, spark: true });
|
||||
}
|
||||
// grains flying into the vacuum nozzle
|
||||
function spawnSuck(x, y, m) {
|
||||
if (!SET.particles || R.particles.length > 500) return;
|
||||
const v = (Math.random() - 0.5) * 2 * CVAR[m];
|
||||
R.particles.push({ x: x + 0.5, y: y + 0.5, vx: 0, vy: 0, life: 40, r: COLR[m] + v, g: COLG[m] + v, b: COLB[m] + v, suck: true });
|
||||
}
|
||||
function drawParticles() {
|
||||
const ctx = R.ctx, zp = R.zp;
|
||||
const ps = R.particles;
|
||||
const tx = bot.x + bot.w / 2, ty = bot.y + bot.h / 2;
|
||||
for (let k = ps.length - 1; k >= 0; k--) {
|
||||
const p = ps[k];
|
||||
if (p.suck) {
|
||||
const dx = tx - p.x, dy = ty - p.y, d = Math.hypot(dx, dy);
|
||||
if (d < 2.5) { ps.splice(k, 1); continue; }
|
||||
const sp = Math.min(d, 0.6 + (40 - p.life) * 0.28);
|
||||
p.x += dx / d * sp; p.y += dy / d * sp; p.life--;
|
||||
if (p.life <= 0) { ps.splice(k, 1); continue; }
|
||||
} else {
|
||||
p.x += p.vx; p.y += p.vy; p.vy += 0.04; p.life--;
|
||||
if (p.life <= 0) { ps.splice(k, 1); continue; }
|
||||
}
|
||||
const s = worldToScreenPx(p.x, p.y);
|
||||
ctx.fillStyle = `rgba(${p.r | 0},${p.g | 0},${p.b | 0},${Math.min(1, p.life / 10)})`;
|
||||
const sz = p.spark ? Math.max(1, zp * 0.5) : p.suck ? zp : Math.max(1, zp * 0.7);
|
||||
ctx.fillRect(s.x, s.y, sz, sz);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
/* Drawing (src/render/): browser only. This file: the renderer state R, colour tables (glow,
|
||||
thermal palettes, pipes, trees, sky), canvas setup and the camera. */
|
||||
|
||||
const R = {
|
||||
canvas: null, ctx: null, buf: null, bctx: null, img: null, px: null,
|
||||
glowC: null, glowX: null, glowImg: null, glowPx: null,
|
||||
bw: 0, bh: 0, zp: 3, camX: 0, camY: 0, vx0: 0, vy0: 0,
|
||||
lr: null, lg: null, lb: null, latt: null, lw: 0, lh: 0, lx0: 0, ly0: 0, lres: 1,
|
||||
frame: 0, lens: false, particles: [],
|
||||
};
|
||||
|
||||
// ---------- LUTs ----------
|
||||
const GLOW_W = new Float32Array(700), GLOW_R = new Float32Array(700), GLOW_G = new Float32Array(700), GLOW_B = new Float32Array(700);
|
||||
(function buildGlow() {
|
||||
const k = [[450, 0, 90, 10, 0], [560, 0.26, 140, 18, 0], [700, 0.46, 190, 34, 4], [850, 0.6, 224, 56, 8], [1000, 0.72, 244, 86, 16],
|
||||
[1150, 0.82, 255, 118, 30], [1300, 0.89, 255, 150, 52], [1500, 0.94, 255, 192, 96], [1750, 0.97, 255, 226, 158], [2200, 1, 255, 250, 228]];
|
||||
for (let n = 0; n < 700; n++) {
|
||||
const T = 400 + n * 4;
|
||||
let a = k[0], b = k[k.length - 1];
|
||||
for (let j = 0; j < k.length - 1; j++) if (T >= k[j][0] && T <= k[j + 1][0]) { a = k[j]; b = k[j + 1]; break; }
|
||||
let f = b[0] === a[0] ? 0 : (T - a[0]) / (b[0] - a[0]);
|
||||
if (T < k[0][0]) { a = b = k[0]; f = 0; }
|
||||
if (T > k[k.length - 1][0]) { a = b = k[k.length - 1]; f = 0; }
|
||||
GLOW_W[n] = a[1] + (b[1] - a[1]) * f; GLOW_R[n] = a[2] + (b[2] - a[2]) * f; GLOW_G[n] = a[3] + (b[3] - a[3]) * f; GLOW_B[n] = a[4] + (b[4] - a[4]) * f;
|
||||
}
|
||||
})();
|
||||
function glowIndex(T) { const n = ((T - 400) / 4) | 0; return n < 0 ? 0 : n > 699 ? 699 : n; }
|
||||
|
||||
const THERM = { inferno: null, ironbow: null, gray: null };
|
||||
(function buildTherm() {
|
||||
const pal = {
|
||||
inferno: [[0, 8, 6, 28], [20, 24, 14, 64], [60, 64, 18, 110], [150, 130, 30, 108], [350, 196, 54, 78], [700, 238, 104, 32], [1100, 250, 172, 30], [1600, 252, 232, 120], [2200, 255, 255, 236]],
|
||||
ironbow: [[0, 0, 0, 16], [20, 26, 0, 92], [60, 92, 0, 150], [150, 172, 20, 140], [350, 224, 70, 60], [700, 250, 140, 0], [1100, 255, 200, 40], [1600, 255, 240, 150], [2200, 255, 255, 255]],
|
||||
gray: [[0, 10, 10, 10], [20, 40, 40, 40], [60, 70, 70, 70], [150, 105, 105, 105], [350, 145, 145, 145], [700, 185, 185, 185], [1100, 215, 215, 215], [1600, 240, 240, 240], [2200, 255, 255, 255]],
|
||||
};
|
||||
for (const name in pal) {
|
||||
const k = pal[name], lut = new Uint8Array(551 * 3);
|
||||
for (let n = 0; n <= 550; n++) {
|
||||
const T = n * 4;
|
||||
let j = 0; while (j < k.length - 2 && T > k[j + 1][0]) j++;
|
||||
const a = k[j], b = k[j + 1];
|
||||
const f = Math.max(0, Math.min(1, (T - a[0]) / (b[0] - a[0])));
|
||||
lut[n * 3] = a[1] + (b[1] - a[1]) * f; lut[n * 3 + 1] = a[2] + (b[2] - a[2]) * f; lut[n * 3 + 2] = a[3] + (b[3] - a[3]) * f;
|
||||
}
|
||||
THERM[name] = lut;
|
||||
}
|
||||
})();
|
||||
function thermIdx(T) { const n = (T / 4) | 0; return (n < 0 ? 0 : n > 550 ? 550 : n) * 3; }
|
||||
|
||||
// Pipes, pumps and outlets, cell by cell: a copper wall round a bore that shows the line's liquid
|
||||
// (dark when empty, brighter the fuller it is, with a ripple while it flows), open toward the
|
||||
// pipes it joins. Pumps are blue-grey with an impeller and a grille on the intake side;
|
||||
// outlets have a dark mouth on the side they pour from.
|
||||
const PC = [0, 0, 0];
|
||||
function pipeCellColor(x, y, anim) {
|
||||
const lx = x & 3, ly = y & 3, b = (y >> 2) * BW + (x >> 2), code = blockP[b];
|
||||
const net = pipeNetOf(b), full = net && net.n > 0 ? Math.min(1, net.n / net.cap) : 0;
|
||||
const flowing = net && tick - net.flowT < 20;
|
||||
const mid = (lx === 1 || lx === 2), midY = (ly === 1 || ly === 2);
|
||||
let bore = mid && midY;
|
||||
if (!bore && code === BP_PIPE) {
|
||||
if (mid && ly === 0) bore = pipeJoins(b, 0) >= 0;
|
||||
else if (mid && ly === 3) bore = pipeJoins(b, 2) >= 0;
|
||||
else if (midY && lx === 3) bore = pipeJoins(b, 1) >= 0;
|
||||
else if (midY && lx === 0) bore = pipeJoins(b, 3) >= 0;
|
||||
}
|
||||
if (code === BP_PUMP || code === BP_OUTLET) {
|
||||
const v = code === BP_PUMP ? [-PUMP_V[blockV[b] & 3][0], -PUMP_V[blockV[b] & 3][1]] : OUTLET_V[blockV[b] & 3];
|
||||
const s = sideOf(v[0], v[1]), edge = s === 0 ? ly === 0 : s === 2 ? ly === 3 : s === 1 ? lx === 3 : lx === 0;
|
||||
const off = (blockV[b] & 4) !== 0;
|
||||
if (edge) {
|
||||
// intake grille / outlet mouth
|
||||
if (code === BP_PUMP) { const bar = (s === 0 || s === 2 ? lx : ly) & 1; PC[0] = bar ? 40 : 92; PC[1] = bar ? 44 : 100; PC[2] = bar ? 50 : 112; }
|
||||
else if (mid || midY) { const lq = flowing && !off && net.lm; PC[0] = lq ? COLR[net.lm] : 22; PC[1] = lq ? COLG[net.lm] : 20; PC[2] = lq ? COLB[net.lm] : 20; }
|
||||
else { PC[0] = 150; PC[1] = 96; PC[2] = 62; }
|
||||
return;
|
||||
}
|
||||
if (code === BP_PUMP) {
|
||||
if (bore) { const spin = flowing && !off ? ((anim >> 1) + lx + ly * 3) & 3 : (lx + ly) & 1; PC[0] = spin === 0 ? 196 : 70; PC[1] = spin === 0 ? 204 : 78; PC[2] = spin === 0 ? 214 : 92; }
|
||||
else { PC[0] = off ? 84 : 104; PC[1] = off ? 90 : 122; PC[2] = off ? 98 : 142; if (lx === 0 || ly === 0) { PC[0] += 18; PC[1] += 18; PC[2] += 18; } }
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (bore) {
|
||||
// a line that empties as fast as it fills still shows its liquid running through
|
||||
const lm = net ? (net.n > 0 ? net.m : flowing ? net.lm : 0) : 0;
|
||||
if (lm) {
|
||||
const k = 0.45 + 0.55 * full, ripple = flowing && ((x + y + (anim >> 2)) & 3) === 0 ? 22 : 0;
|
||||
PC[0] = COLR[lm] * k + ripple; PC[1] = COLG[lm] * k + ripple; PC[2] = COLB[lm] * k + ripple;
|
||||
} else { PC[0] = 30; PC[1] = 26; PC[2] = 24; }
|
||||
return;
|
||||
}
|
||||
// copper wall, lit on its top and left edges
|
||||
const hi = (lx === 0 || ly === 0) ? 24 : (lx === 3 || ly === 3) ? -22 : 0;
|
||||
PC[0] = 170 + hi; PC[1] = 106 + hi * 0.7; PC[2] = 66 + hi * 0.5;
|
||||
}
|
||||
// A pipe block is thin: only a cross two grains wide (toward the pipes it joins) is pipe; the
|
||||
// rest of the block shows whatever is there, water flowing past included.
|
||||
function pipeBodyCell(x, y) {
|
||||
const lx = x & 3, ly = y & 3, mx = lx === 1 || lx === 2, my = ly === 1 || ly === 2;
|
||||
if (mx && my) return true;
|
||||
const b = (y >> 2) * BW + (x >> 2);
|
||||
if (mx) return ly === 0 ? pipeJoins(b, 0) >= 0 : pipeJoins(b, 2) >= 0;
|
||||
if (my) return lx === 3 ? pipeJoins(b, 1) >= 0 : pipeJoins(b, 3) >= 0;
|
||||
return false;
|
||||
}
|
||||
// a thin pipe's own cells: copper, lit on one side, with the line's liquid showing through the bore
|
||||
function thinPipeColor(x, y, anim) {
|
||||
const lx = x & 3, ly = y & 3, b = (y >> 2) * BW + (x >> 2), net = pipeNetOf(b);
|
||||
const flowing = net && tick - net.flowT < 20, lm = net ? (net.n > 0 ? net.m : flowing ? net.lm : 0) : 0;
|
||||
const vert = (lx === 1 || lx === 2) && (ly === 0 || ly === 3), first = vert ? lx === 1 : ly === 1;
|
||||
let r = first ? 194 : 150, g = first ? 124 : 92, bl = first ? 80 : 56;
|
||||
if (lm) {
|
||||
const k = 0.4 + 0.35 * Math.min(1, net.n / net.cap), rip = flowing && ((x + y + (anim >> 2)) & 3) === 0 ? 20 : 0;
|
||||
r = r * (1 - k) + COLR[lm] * k + rip; g = g * (1 - k) + COLG[lm] * k + rip; bl = bl * (1 - k) + COLB[lm] * k + rip;
|
||||
}
|
||||
PC[0] = r; PC[1] = g; PC[2] = bl;
|
||||
}
|
||||
// a block that is a belt or a filter: they join up into one belt
|
||||
function beltLike(b) { if (b < 0 || b >= NB) return false; const c = blockP[b]; return c === BP_BELT || c === BP_FILTER; }
|
||||
// standing trees: the trunk shows its dark bark, so it reads apart from the pale cut wood it drops
|
||||
const TREE_R = new Uint8Array(NMAT), TREE_G = new Uint8Array(NMAT), TREE_B = new Uint8Array(NMAT);
|
||||
for (let m = 0; m < NMAT; m++) { TREE_R[m] = COLR[m]; TREE_G[m] = COLG[m]; TREE_B[m] = COLB[m]; }
|
||||
TREE_R[WOOD] = 112; TREE_G[WOOD] = 82; TREE_B[WOOD] = 60;
|
||||
// background colors per layer (cave back walls)
|
||||
const BG = {
|
||||
dirt: [46, 33, 24], stone: [36, 36, 40], granite: [48, 38, 38], basalt: [22, 22, 26],
|
||||
};
|
||||
let SKY_R, SKY_G, SKY_B;
|
||||
function buildSky() {
|
||||
SKY_R = new Uint8Array(H); SKY_G = new Uint8Array(H); SKY_B = new Uint8Array(H);
|
||||
let sm = 0; for (let x = 0; x < W; x++) sm += surfY[x]; sm /= W;
|
||||
for (let y = 0; y < H; y++) {
|
||||
const f = Math.min(1, y / (sm + 20));
|
||||
SKY_R[y] = 88 + f * 92; SKY_G[y] = 140 + f * 70; SKY_B[y] = 198 + f * 34;
|
||||
}
|
||||
}
|
||||
|
||||
function initRender(canvas) {
|
||||
R.canvas = canvas;
|
||||
R.ctx = canvas.getContext('2d', { alpha: false });
|
||||
R.buf = document.createElement('canvas');
|
||||
R.bctx = R.buf.getContext('2d');
|
||||
R.glowC = document.createElement('canvas');
|
||||
R.glowX = R.glowC.getContext('2d');
|
||||
R.lightC = document.createElement('canvas');
|
||||
R.lightX = R.lightC.getContext('2d');
|
||||
}
|
||||
|
||||
function resizeRender() {
|
||||
const dpr = Math.min(window.devicePixelRatio || 1, 3) * SET.renderScale;
|
||||
const cw = Math.max(1, Math.floor(window.innerWidth * dpr)), ch = Math.max(1, Math.floor(window.innerHeight * dpr));
|
||||
if (R.canvas.width !== cw || R.canvas.height !== ch) { R.canvas.width = cw; R.canvas.height = ch; }
|
||||
let zp = SET.zoom * dpr;
|
||||
if (SET.integerScale) zp = Math.max(1, Math.round(zp));
|
||||
R.zp = zp;
|
||||
const bw = Math.ceil(cw / zp) + 2, bh = Math.ceil(ch / zp) + 2;
|
||||
if (bw !== R.bw || bh !== R.bh) {
|
||||
R.bw = bw; R.bh = bh;
|
||||
R.buf.width = bw; R.buf.height = bh;
|
||||
R.img = R.bctx.createImageData(bw, bh);
|
||||
R.px = new Uint32Array(R.img.data.buffer);
|
||||
// the smooth look also needs, per grain: the colour behind it, any tree, and occupancy fields
|
||||
R.backPx = new Uint32Array(bw * bh); R.treePx = new Uint32Array(bw * bh); R.fieldPx = new Uint32Array(bw * bh); R.moldMask = new Uint8Array(bw * bh);
|
||||
const gw = Math.ceil(bw / 2), gh = Math.ceil(bh / 2);
|
||||
R.glowC.width = gw; R.glowC.height = gh;
|
||||
R.glowImg = R.glowX.createImageData(gw, gh);
|
||||
R.glowPx = new Uint32Array(R.glowImg.data.buffer);
|
||||
R.lw = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function viewCells() { return { w: R.canvas.width / R.zp, h: R.canvas.height / R.zp }; }
|
||||
|
||||
// lookX/lookY: how far the pointer is from screen center, in grains (camera leads toward it)
|
||||
function updateCamera(dt, lookX, lookY) {
|
||||
const v = viewCells();
|
||||
let tx = bot.x + bot.w / 2 - v.w / 2, ty = bot.y + bot.h / 2 - v.h / 2;
|
||||
if (lookX !== undefined) { tx += lookX * 0.22; ty += lookY * 0.22; }
|
||||
let k = SET.cameraSmooth >= 1 ? 1 : 1 - Math.pow(SET.cameraSmooth * 0.92, dt * 60);
|
||||
if (R.snapNext) { k = 1; R.snapNext = false; } // after a zoom, jump straight to the bot
|
||||
R.camX += (tx - R.camX) * k; R.camY += (ty - R.camY) * k;
|
||||
if (v.w >= W) R.camX = (W - v.w) / 2; else R.camX = Math.max(0, Math.min(W - v.w, R.camX));
|
||||
if (v.h >= H) R.camY = (H - v.h) / 2; else R.camY = Math.max(0, Math.min(H - v.h, R.camY));
|
||||
if (R.shake > 0) { R.camX += (Math.random() - 0.5) * R.shake; R.camY += (Math.random() - 0.5) * R.shake; R.shake *= 0.85; if (R.shake < 0.05) R.shake = 0; }
|
||||
}
|
||||
function snapCamera() {
|
||||
const v = viewCells();
|
||||
R.camX = bot.x + bot.w / 2 - v.w / 2; R.camY = bot.y + bot.h / 2 - v.h / 2;
|
||||
R.camX = Math.max(0, Math.min(Math.max(0, W - v.w), R.camX)); R.camY = Math.max(0, Math.min(Math.max(0, H - v.h), R.camY));
|
||||
}
|
||||
function screenToWorld(sx, sy) {
|
||||
const dpr = R.canvas.width / window.innerWidth;
|
||||
return { x: R.camX + sx * dpr / R.zp, y: R.camY + sy * dpr / R.zp };
|
||||
}
|
||||
function worldToScreenPx(x, y) { return { x: (x - R.camX) * R.zp, y: (y - R.camY) * R.zp }; }
|
||||
@@ -0,0 +1,430 @@
|
||||
/* =====================================================================
|
||||
Smooth look: a WebGL2 pass that turns the per-grain buffers drawWorld
|
||||
fills (colour + class, colour behind, trees, occupancy fields, light,
|
||||
glow) into rounded, outlined, softly lit shapes at screen resolution.
|
||||
The 2D canvas then draws this image as its world layer, and every
|
||||
overlay (machines, bot, previews, labels) goes on top as before.
|
||||
===================================================================== */
|
||||
|
||||
const SM = { tried: false, ok: false, why: '', gl: null, canvas: null, prog: null, tex: {}, u: {}, t0: performance.now(), white: null, surf: -1, scale: 1, ms: 0, n: 0 };
|
||||
|
||||
const SM_VS = `#version 300 es
|
||||
void main() {
|
||||
vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
|
||||
gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);
|
||||
}`;
|
||||
|
||||
const SM_FS = `#version 300 es
|
||||
precision highp float;
|
||||
precision highp int;
|
||||
uniform sampler2D tCol, tColL, tBack, tTree, tField, tLight, tGlow;
|
||||
uniform vec2 uSize, uOrigin, uLightSize, uGlowSize;
|
||||
uniform vec4 uView, uLightMap;
|
||||
uniform float uGlowK, uTime, uSurf, uRain;
|
||||
out vec4 o;
|
||||
|
||||
float h21(vec2 p) { p = fract(p * vec2(123.34, 456.21)); p += dot(p, p + 45.32); return fract(p.x * p.y); }
|
||||
float vn(vec2 p) {
|
||||
vec2 i = floor(p), f = fract(p); f = f * f * (3.0 - 2.0 * f);
|
||||
return mix(mix(h21(i), h21(i + vec2(1, 0)), f.x), mix(h21(i + vec2(0, 1)), h21(i + vec2(1, 1)), f.x), f.y);
|
||||
}
|
||||
float fbm(vec2 p) { float a = 0.5, s = 0.0; for (int k = 0; k < 4; k++) { s += a * vn(p); p = p * 2.03 + 17.1; a *= 0.5; } return s / 0.9375; }
|
||||
|
||||
vec4 lin(sampler2D t, vec2 q) { return textureLod(t, q / uSize, 0.0); }
|
||||
// cubic B-spline from four bilinear taps: rounds a field of 0s and 1s into smooth shapes
|
||||
vec4 bsp(sampler2D t, vec2 q) {
|
||||
vec2 tc = q - 0.5, i = floor(tc), f = tc - i, f2 = f * f, f3 = f2 * f;
|
||||
vec2 w0 = (1.0 - 3.0 * f + 3.0 * f2 - f3) / 6.0, w1 = (4.0 - 6.0 * f2 + 3.0 * f3) / 6.0;
|
||||
vec2 w2 = (1.0 + 3.0 * f + 3.0 * f2 - 3.0 * f3) / 6.0, w3 = f3 / 6.0;
|
||||
vec2 g0 = w0 + w1, g1 = w2 + w3;
|
||||
vec2 h0 = (i - 0.5 + w1 / g0) / uSize, h1 = (i + 1.5 + w3 / g1) / uSize;
|
||||
return g0.y * (g0.x * textureLod(t, h0, 0.0) + g1.x * textureLod(t, vec2(h1.x, h0.y), 0.0))
|
||||
+ g1.y * (g0.x * textureLod(t, vec2(h0.x, h1.y), 0.0) + g1.x * textureLod(t, h1, 0.0));
|
||||
}
|
||||
vec4 cellAt(sampler2D t, ivec2 c) { return texelFetch(t, clamp(c, ivec2(0), ivec2(uSize) - 1), 0); }
|
||||
int cls(vec4 c) { return int(c.a * 255.0 + 0.5); }
|
||||
float isT(int n) { return (n == 1 || n == 4 || n == 6 || (n >= 7 && n <= 12)) ? 1.0 : 0.0; }
|
||||
float isM(int n) { return n == 2 ? 1.0 : isT(n); } // matter: ground, built pieces or loose grains
|
||||
float isF(int n) { return (n == 13 || n == 14) ? 1.0 : 0.0; }
|
||||
float isBelt(int n) { return (n == 7 || n == 8) ? 1.0 : 0.0; }
|
||||
float isN(int n, int k) { return n == k ? 1.0 : 0.0; }
|
||||
// signed distance (in grains) to the outline of one block of belt: a band 3.7 grains tall; where
|
||||
// the line of belts ends, the end is square with rounded corners. Negative inside.
|
||||
float beltSD(vec2 lb, bool endL, bool endR) {
|
||||
float x0 = endL ? 0.15 : -2.0, x1 = endR ? 3.85 : 6.0, r = 0.75;
|
||||
vec2 c = vec2((x0 + x1) * 0.5, 2.0), hs = vec2((x1 - x0) * 0.5, 1.85);
|
||||
vec2 q = abs(lb - c) - hs + r;
|
||||
return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r;
|
||||
}
|
||||
// The outline of the belt or filter in cell c. Belts and filters side by side are one belt, so
|
||||
// the line only rounds off where the whole run ends.
|
||||
float beltLineSD(ivec2 c, vec2 lb) {
|
||||
int nr = cls(cellAt(tCol, c + ivec2(4, 0))), nl = cls(cellAt(tCol, c - ivec2(4, 0)));
|
||||
return beltSD(lb, isBelt(nl) + isF(nl) < 0.5, isBelt(nr) + isF(nr) < 0.5);
|
||||
}
|
||||
// The rubber band of a belt or filter: 1 on a ridge. Ridges travel along the top run one way and
|
||||
// back along the bottom.
|
||||
float bandRidge(vec2 w, vec2 lb, float dir) {
|
||||
float run = lb.y < 2.0 ? 1.0 : -1.0;
|
||||
return step(0.72, fract((w.x - dir * run * uTime * 30.0) * 0.5));
|
||||
}
|
||||
vec3 bandColor(vec2 lb, float rg) { return mix(vec3(0.2, 0.21, 0.24), vec3(0.34, 0.35, 0.39), rg) * (lb.y < 2.0 ? 1.0 : 0.72); }
|
||||
|
||||
void main() {
|
||||
float zp = uView.z, px = 1.0 / zp;
|
||||
vec2 sp = vec2(gl_FragCoord.x, uView.w - gl_FragCoord.y);
|
||||
vec2 q = sp * px + uView.xy; // position in grains, in the window of grains uploaded
|
||||
vec2 w = q + uOrigin; // position in the world
|
||||
vec2 cam = uOrigin + uView.xy; // world position of the screen's top-left corner
|
||||
vec2 scr = q - uView.xy; // grains from the screen's top-left corner
|
||||
float shc = uView.w * px; // screen height in grains
|
||||
ivec2 c = ivec2(floor(q));
|
||||
vec2 qq = q - 0.5; ivec2 c0 = ivec2(floor(qq)); vec2 t = qq - floor(qq);
|
||||
vec4 k0 = cellAt(tCol, c0), k1 = cellAt(tCol, c0 + ivec2(1, 0)), k2 = cellAt(tCol, c0 + ivec2(0, 1)), k3 = cellAt(tCol, c0 + ivec2(1, 1));
|
||||
int n0 = cls(k0), n1 = cls(k1), n2 = cls(k2), n3 = cls(k3);
|
||||
vec4 own = cellAt(tCol, c); int nc = cls(own);
|
||||
vec2 s = smoothstep(0.1, 0.9, t);
|
||||
vec4 wq = vec4((1.0 - s.x) * (1.0 - s.y), s.x * (1.0 - s.y), (1.0 - s.x) * s.y, s.x * s.y);
|
||||
|
||||
// fields, and everything that needs screen derivatives (worked out before any branching)
|
||||
vec4 Fb = bsp(tField, q);
|
||||
vec4 F = mix(lin(tField, q), Fb, 0.62);
|
||||
// loose-grain occupancy from the four grains around (bilinear): ground and grains together make "matter"
|
||||
vec4 wl = vec4((1.0 - t.x) * (1.0 - t.y), t.x * (1.0 - t.y), (1.0 - t.x) * t.y, t.x * t.y);
|
||||
float pw = dot(wl, vec4(isN(n0, 2), isN(n1, 2), isN(n2, 2), isN(n3, 2)));
|
||||
float M = clamp(F.r + pw, 0.0, 1.0);
|
||||
float lf = clamp(mix(F.g, Fb.g, 0.6) + F.r + pw, 0.0, 1.0);
|
||||
float fwT = max(fwidth(F.r), 1e-4), fwM = max(fwidth(M), 1e-4), fwA = max(fwidth(F.a), 1e-4), fwL = max(fwidth(lf), 1e-4);
|
||||
vec2 gT = vec2(dFdx(mix(Fb.r, M, 0.5)), dFdy(mix(Fb.r, M, 0.5))), gL = vec2(dFdx(F.g), dFdy(F.g));
|
||||
|
||||
// ---- behind everything: sky with clouds and far hills, or the cave's back wall
|
||||
vec4 back = lin(tBack, q);
|
||||
vec3 C = back.rgb;
|
||||
float sky = back.a;
|
||||
if (sky > 0.01) {
|
||||
vec3 S = C;
|
||||
vec2 P0 = scr + cam * 0.08;
|
||||
// clouds; while it rains they thicken and turn grey
|
||||
float cl = smoothstep(0.56 - 0.3 * uRain, 0.78 - 0.2 * uRain, fbm(vec2(P0.x * 0.016 + uTime * 0.006, P0.y * 0.05)));
|
||||
float hi = clamp((uSurf - 40.0 - w.y) / 50.0, 0.0, 1.0);
|
||||
S = mix(S, mix(vec3(0.96, 0.95, 0.93), vec3(0.5, 0.52, 0.56), uRain), cl * (0.6 + 0.3 * uRain) * hi);
|
||||
float k1 = 0.16; vec2 P1 = scr + cam * k1;
|
||||
float base1 = shc * 0.5 + 8.0 + (uSurf - shc * 0.5) * k1;
|
||||
float top1 = base1 - 36.0 * (0.25 + 0.9 * fbm(vec2(P1.x * 0.009, 1.7)));
|
||||
float m1 = smoothstep(top1 - px * 1.5, top1 + px * 1.5, P1.y);
|
||||
vec3 far = mix(vec3(0.50, 0.60, 0.72), S, 0.35) * (0.94 + 0.08 * fbm(P1 * 0.05));
|
||||
S = mix(S, far, m1);
|
||||
float k2 = 0.38; vec2 P2 = scr + cam * k2;
|
||||
float base2 = shc * 0.5 + 14.0 + (uSurf - shc * 0.5) * k2;
|
||||
float top2 = base2 - 20.0 * (0.2 + 0.9 * fbm(vec2(P2.x * 0.016, 7.3)));
|
||||
float m2 = smoothstep(top2 - px * 1.5, top2 + px * 1.5, P2.y);
|
||||
vec3 near = vec3(0.36, 0.5, 0.47) * (0.92 + 0.14 * fbm(P2 * 0.08));
|
||||
near = mix(near, S, 0.42 - 0.2 * smoothstep(top2, top2 + 30.0, P2.y));
|
||||
S = mix(S, near, m2);
|
||||
C = mix(C, S, sky);
|
||||
}
|
||||
// the back wall: rough rock, and darker close to the tunnel walls (ambient occlusion)
|
||||
float notSky = 1.0 - sky;
|
||||
C *= mix(1.0, 1.08 + 0.4 * fbm(w * 0.21), notSky);
|
||||
float near = textureLod(tField, q / uSize, 2.2).r;
|
||||
C *= 1.0 - 0.34 * smoothstep(0.12, 0.85, near) * (1.0 - 0.75 * sky);
|
||||
|
||||
// ---- trees (behind the ground and everything on it)
|
||||
if (F.a > 0.2) {
|
||||
vec4 a0 = cellAt(tTree, c0), a1 = cellAt(tTree, c0 + ivec2(1, 0)), a2 = cellAt(tTree, c0 + ivec2(0, 1)), a3 = cellAt(tTree, c0 + ivec2(1, 1));
|
||||
vec4 m = vec4(step(0.4, a0.a), step(0.4, a1.a), step(0.4, a2.a), step(0.4, a3.a)) * wq;
|
||||
float sm = m.x + m.y + m.z + m.w;
|
||||
if (sm > 1e-4) {
|
||||
vec3 tc = (a0.rgb * m.x + a1.rgb * m.y + a2.rgb * m.z + a3.rgb * m.w) / sm;
|
||||
float leaf = (a0.a * m.x + a1.a * m.y + a2.a * m.z + a3.a * m.w) / sm;
|
||||
float lv = smoothstep(0.6, 0.9, leaf);
|
||||
tc *= mix(0.86 + 0.22 * vn(vec2(w.x * 1.4, w.y * 0.25)), 0.72 + 0.5 * fbm(w * 0.42), lv);
|
||||
float d = (F.a - 0.36) / fwA;
|
||||
tc *= mix(0.55, 1.0, smoothstep(0.4, 1.6, d));
|
||||
C = mix(C, tc, smoothstep(-0.7, 0.7, d));
|
||||
}
|
||||
}
|
||||
|
||||
// ---- smoke, steam and flame: soft, see-through puffs
|
||||
if (Fb.b > 0.02) {
|
||||
vec4 m = vec4(isN(n0, 5), isN(n1, 5), isN(n2, 5), isN(n3, 5)) * wq;
|
||||
float sm = m.x + m.y + m.z + m.w;
|
||||
vec3 gc = sm > 1e-4 ? (k0.rgb * m.x + k1.rgb * m.y + k2.rgb * m.z + k3.rgb * m.w) / sm : own.rgb;
|
||||
if (sm > 1e-4 || nc == 5) C = mix(C, gc, smoothstep(0.03, 0.55, Fb.b) * 0.92);
|
||||
}
|
||||
|
||||
// ---- liquids: smooth surfaces that fill right up against the ground, with a glossy top line
|
||||
float anyL = isN(n0, 3) + isN(n1, 3) + isN(n2, 3) + isN(n3, 3);
|
||||
if (anyL > 0.0 && lf > 0.3) {
|
||||
vec4 m = vec4(isN(n0, 3), isN(n1, 3), isN(n2, 3), isN(n3, 3)) * (wq + 0.02);
|
||||
float sm = m.x + m.y + m.z + m.w;
|
||||
vec3 lc = (k0.rgb * m.x + k1.rgb * m.y + k2.rgb * m.z + k3.rgb * m.w) / sm;
|
||||
lc *= 0.94 + 0.1 * vn(w * 0.55 + vec2(uTime * 0.7, uTime * 0.25));
|
||||
float gl = length(gL);
|
||||
float topSide = gl > 1e-5 ? clamp(gL.y / gl, 0.0, 1.0) : 0.0;
|
||||
float band = 1.0 - smoothstep(0.5, 0.75, F.g);
|
||||
lc += vec3(0.16, 0.18, 0.2) * topSide * band;
|
||||
C = mix(C, lc, smoothstep(0.5 - fwL, 0.5 + fwL, lf));
|
||||
}
|
||||
|
||||
// ---- the ground: rounded, outlined, with a lit top edge and a shaded underside
|
||||
if (F.r > 0.3) {
|
||||
vec4 m = vec4(isT(n0), isT(n1), isT(n2), isT(n3)) * wq;
|
||||
float sm = m.x + m.y + m.z + m.w;
|
||||
vec3 blur = bsp(tColL, q).rgb, tc = own.rgb;
|
||||
float bd = 1e9;
|
||||
if (m.x > 0.0) { vec3 e = k0.rgb - blur; float dd = dot(e, e); if (dd < bd) { bd = dd; tc = k0.rgb; } }
|
||||
if (m.y > 0.0) { vec3 e = k1.rgb - blur; float dd = dot(e, e); if (dd < bd) { bd = dd; tc = k1.rgb; } }
|
||||
if (m.z > 0.0) { vec3 e = k2.rgb - blur; float dd = dot(e, e); if (dd < bd) { bd = dd; tc = k2.rgb; } }
|
||||
if (m.w > 0.0) { vec3 e = k3.rgb - blur; float dd = dot(e, e); if (dd < bd) { bd = dd; tc = k3.rgb; } }
|
||||
if (sm < 1e-4) tc = own.rgb;
|
||||
tc *= 0.84 + 0.3 * fbm(w * 0.28);
|
||||
tc *= 0.95 + 0.1 * vn(w * 1.6);
|
||||
float gl = length(gT);
|
||||
vec2 nrm = gl > 1e-6 ? -gT / gl : vec2(0.0);
|
||||
float band = 1.0 - smoothstep(0.5, 0.9, M);
|
||||
float lit = dot(nrm, normalize(vec2(-0.5, -1.0)));
|
||||
tc *= 1.0 + 0.42 * lit * band;
|
||||
// the outline follows where ground and grains meet open space, never where rock meets gravel
|
||||
float dm = (M - 0.5) / fwM;
|
||||
tc *= mix(0.4, 1.0, smoothstep(0.5, 1.4 + zp * 0.08, dm));
|
||||
C = mix(C, tc, smoothstep(-0.6, 0.6, (F.r - 0.5) / fwT));
|
||||
}
|
||||
|
||||
// ---- loose grains: little round pebbles; a pocket closed in by four grains is filled
|
||||
int np = int(isN(n0, 2) + isN(n1, 2) + isN(n2, 2) + isN(n3, 2));
|
||||
bool closed = isM(n0) + isM(n1) + isM(n2) + isM(n3) > 3.5;
|
||||
if (np > 0) {
|
||||
float best = 1e9; vec3 gc = vec3(0.0); vec2 gd = vec2(0.0); float gr = 0.6;
|
||||
for (int k = 0; k < 4; k++) {
|
||||
ivec2 o4 = ivec2(k & 1, k >> 1);
|
||||
int nk = k == 0 ? n0 : k == 1 ? n1 : k == 2 ? n2 : n3;
|
||||
if (nk != 2) continue;
|
||||
vec4 ck = k == 0 ? k0 : k == 1 ? k1 : k == 2 ? k2 : k3;
|
||||
vec2 ce = vec2(c0 + o4) + 0.5;
|
||||
float r = 0.6 + 0.1 * (h21(ce + uOrigin) - 0.5);
|
||||
vec2 dv = (q - ce) / r;
|
||||
float dd = length(dv);
|
||||
if (dd < best) { best = dd; gc = ck.rgb; gd = dv; gr = r; }
|
||||
}
|
||||
float cover = np == 4 ? 1.0 : 1.0 - smoothstep(1.0 - px / gr, 1.0 + px / gr, best);
|
||||
if (closed && F.r < 0.5) cover = 1.0;
|
||||
if (cover > 0.0) {
|
||||
float r2 = min(1.0, dot(gd, gd));
|
||||
vec3 n3 = normalize(vec3(gd * 0.85, sqrt(1.0 - r2) + 0.25));
|
||||
float sh = np == 4 ? 0.9 + 0.08 * vn(w * 0.9) : 0.8 + 0.3 * max(0.0, dot(n3, normalize(vec3(-0.45, -0.8, 0.6))));
|
||||
vec3 pc = (np == 4 ? (k0.rgb * wq.x + k1.rgb * wq.y + k2.rgb * wq.z + k3.rgb * wq.w) : gc) * sh;
|
||||
if (!closed) pc *= mix(1.0, 0.72, smoothstep(0.7, 1.0, best));
|
||||
C = mix(C, pc, cover);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- built pieces: crisp, and wall blocks drawn as bevelled tiles with mortar lines
|
||||
if (nc == 4) C = own.rgb;
|
||||
else if (nc == 7 || nc == 8) {
|
||||
// belt: a rubber band round two copper rollers per block; the top run moves the load
|
||||
float dir = nc == 8 ? 1.0 : -1.0;
|
||||
vec2 lb = fract(w * 0.25) * 4.0;
|
||||
// outline: square ends with rounded corners where the line of belts (and filters) ends
|
||||
float sd = beltLineSD(c, lb);
|
||||
float inside = 1.0 - smoothstep(-px, px, sd);
|
||||
if (inside > 0.0) {
|
||||
float edge = -sd;
|
||||
vec3 bc;
|
||||
if (edge < 0.8) {
|
||||
bc = bandColor(lb, bandRidge(w, lb, dir));
|
||||
bc *= mix(0.55, 1.0, smoothstep(0.0, max(px * 1.4, 0.12), edge));
|
||||
} else {
|
||||
vec2 rc = vec2(lb.x < 2.0 ? 1.0 : 3.0, 2.0), dv = lb - rc;
|
||||
float rd = length(dv);
|
||||
bc = vec3(0.15, 0.16, 0.18);
|
||||
if (rd < 0.95) {
|
||||
float ang = atan(dv.y, dv.x) - dir * uTime * 30.0 / 0.95;
|
||||
float spoke = smoothstep(0.55, 0.85, cos(ang * 3.0));
|
||||
bc = mix(vec3(0.5, 0.36, 0.25), vec3(0.72, 0.53, 0.36), 0.5 + 0.5 * dot(normalize(dv + 1e-4), vec2(-0.6, -0.8)) * smoothstep(0.2, 0.95, rd));
|
||||
bc *= mix(1.0, 0.72, spoke * smoothstep(0.25, 0.4, rd));
|
||||
bc *= mix(1.0, 0.55, smoothstep(0.82, 0.95, rd));
|
||||
}
|
||||
}
|
||||
C = mix(C, bc, inside);
|
||||
}
|
||||
}
|
||||
else if (nc >= 9 && nc <= 12) {
|
||||
// screw tube: a copper pipe; what is inside shows through, and the screw's flights turn
|
||||
bool vert = nc == 9 || nc == 11;
|
||||
float sp2 = nc == 9 ? -1.0 : nc == 10 ? 1.0 : nc == 11 ? 1.0 : -1.0;
|
||||
float a = (vert ? fract(w.x * 0.25) : fract(w.y * 0.25)) * 4.0, along = vert ? w.y : w.x;
|
||||
float wall = 0.62;
|
||||
vec3 tc;
|
||||
if (a < wall || a > 4.0 - wall) {
|
||||
float u2 = a < wall ? a / wall : (4.0 - a) / wall; // 0 at the outside, 1 at the bore
|
||||
float cyl = sin(u2 * 1.5708);
|
||||
tc = vec3(0.62, 0.41, 0.27) * (0.6 + 0.55 * cyl);
|
||||
if (a < wall) tc *= 1.08;
|
||||
float ring = 1.0 - smoothstep(0.0, 0.3, min(fract(along * 0.25), 1.0 - fract(along * 0.25)) * 4.0);
|
||||
tc *= 1.0 - 0.3 * ring;
|
||||
tc *= mix(0.5, 1.0, smoothstep(0.0, max(px * 1.3, 0.1), min(a, 4.0 - a)));
|
||||
} else {
|
||||
bool full = own.r + own.g + own.b > 0.0;
|
||||
float fl = fract((along + a * 0.9 - sp2 * uTime * 14.0) * 0.5);
|
||||
float flight = smoothstep(0.82, 0.92, fl) * (1.0 - smoothstep(0.92, 1.0, fl));
|
||||
vec3 bore = vec3(0.11, 0.1, 0.11) * (0.8 + 0.4 * (1.0 - abs(a - 2.0) / 1.4));
|
||||
tc = full ? own.rgb * (0.9 + 0.1 * vn(w * 1.3)) : bore;
|
||||
tc = mix(tc, vec3(0.6, 0.48, 0.38), flight * (full ? 0.35 : 0.8));
|
||||
tc *= 1.0 - 0.25 * smoothstep(0.9, 1.3, abs(a - 2.0));
|
||||
}
|
||||
C = tc;
|
||||
}
|
||||
else if (nc == 6) {
|
||||
vec2 lb = fract(w * 0.25) * 4.0, rb = 4.0 - lb;
|
||||
float e = min(min(lb.x, lb.y), min(rb.x, rb.y));
|
||||
vec3 tc = own.rgb * (0.92 + 0.14 * fbm(w * 0.55));
|
||||
float hl = max(1.0 - smoothstep(0.0, 0.8, lb.x), 1.0 - smoothstep(0.0, 0.8, lb.y));
|
||||
float shd = max(1.0 - smoothstep(0.0, 0.8, rb.x), 1.0 - smoothstep(0.0, 0.8, rb.y));
|
||||
tc *= 1.0 + 0.2 * hl - 0.26 * shd;
|
||||
tc *= mix(0.5, 1.0, smoothstep(px * 0.5, px * 0.5 + max(px * 1.2, 0.14), e));
|
||||
C = tc;
|
||||
}
|
||||
|
||||
else if (nc == 13 || nc == 14) {
|
||||
// filter: a belt whose middle is a see-through steel mesh. It has the same band as a belt, so a
|
||||
// row of belts and filters reads as one belt, rounded only where the whole line ends. The
|
||||
// band is perforated; its colour tag sits on the top run for what rides along, on the
|
||||
// bottom run for what drops through.
|
||||
float dir = nc == 14 ? 1.0 : -1.0;
|
||||
vec2 lb = fract(w * 0.25) * 4.0;
|
||||
vec4 tag = cellAt(tTree, c);
|
||||
bool carry = tag.a > 0.16, tinted = tag.r + tag.g + tag.b > 0.0;
|
||||
float sd = beltLineSD(c, lb);
|
||||
float inside = 1.0 - smoothstep(-px, px, sd);
|
||||
if (inside > 0.0) {
|
||||
float edge = -sd;
|
||||
vec3 steel = vec3(0.42, 0.44, 0.5);
|
||||
// through the holes: whatever is behind (water, sky, rock), or what is passing through
|
||||
vec3 behind = own.r + own.g + own.b > 0.0 ? own.rgb : C * 0.8;
|
||||
vec2 cg = abs(fract(w) - 0.5);
|
||||
float bw2 = max(px * 0.9, 0.09);
|
||||
float hole = 1.0 - smoothstep(0.5 - bw2 - px, 0.5 - bw2, max(cg.x, cg.y));
|
||||
vec3 fc;
|
||||
if (edge < 0.8) {
|
||||
float rg = bandRidge(w, lb, dir);
|
||||
vec3 band = bandColor(lb, rg);
|
||||
if (tinted && (carry == (lb.y < 2.0))) band = mix(band, tag.rgb * (lb.y < 2.0 ? 1.0 : 0.8), 0.75 * rg);
|
||||
fc = mix(band, behind * 0.85, hole * (1.0 - rg) * 0.75);
|
||||
fc *= mix(0.55, 1.0, smoothstep(0.0, max(px * 1.4, 0.12), edge));
|
||||
} else {
|
||||
fc = mix(steel * 0.78, behind, hole);
|
||||
fc *= mix(0.7, 1.0, smoothstep(0.0, max(px * 1.4, 0.12), edge - 0.8));
|
||||
}
|
||||
C = mix(C, fc, inside);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- light, then the glow of hot things on top
|
||||
if (uLightMap.w > 0.5) C *= textureLod(tLight, (q - uLightMap.xy) / uLightMap.z / uLightSize, 0.0).rgb;
|
||||
if (uGlowK > 0.0) {
|
||||
vec2 gu = q / (2.0 * uGlowSize), d = 1.0 / uGlowSize;
|
||||
vec3 g = textureLod(tGlow, gu, 0.0).rgb * 0.25;
|
||||
g += (textureLod(tGlow, gu + vec2(d.x, 0), 0.0).rgb + textureLod(tGlow, gu - vec2(d.x, 0), 0.0).rgb + textureLod(tGlow, gu + vec2(0, d.y), 0.0).rgb + textureLod(tGlow, gu - vec2(0, d.y), 0.0).rgb) * 0.125;
|
||||
g += (textureLod(tGlow, gu + d, 0.0).rgb + textureLod(tGlow, gu - d, 0.0).rgb + textureLod(tGlow, gu + vec2(d.x, -d.y), 0.0).rgb + textureLod(tGlow, gu + vec2(-d.x, d.y), 0.0).rgb) * 0.0625;
|
||||
C += g * uGlowK;
|
||||
}
|
||||
o = vec4(C, 1.0);
|
||||
}`;
|
||||
|
||||
function smCompile(gl, type, src) {
|
||||
const sh = gl.createShader(type);
|
||||
gl.shaderSource(sh, src); gl.compileShader(sh);
|
||||
if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(sh));
|
||||
return sh;
|
||||
}
|
||||
// Is the smooth look possible here (WebGL2)? Set up once, on first use.
|
||||
function smoothAvailable() {
|
||||
if (SM.tried) return SM.ok;
|
||||
SM.tried = true;
|
||||
try {
|
||||
const cv = document.createElement('canvas');
|
||||
const gl = cv.getContext('webgl2', { alpha: false, antialias: false, depth: false, stencil: false, preserveDrawingBuffer: true, premultipliedAlpha: false });
|
||||
if (!gl) { SM.why = 'nogl'; throw new Error('WebGL2 is not available'); }
|
||||
// a browser drawing without the graphics card would crawl: keep the pixel look there
|
||||
const dbg = gl.getExtension('WEBGL_debug_renderer_info');
|
||||
const rend = dbg ? String(gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL)) : '';
|
||||
if (/swiftshader|llvmpipe|softpipe|software/i.test(rend) && !window.__allowSoftwareGL) { SM.why = 'software'; throw new Error('software rendering: ' + rend); }
|
||||
const prog = gl.createProgram();
|
||||
gl.attachShader(prog, smCompile(gl, gl.VERTEX_SHADER, SM_VS));
|
||||
gl.attachShader(prog, smCompile(gl, gl.FRAGMENT_SHADER, SM_FS));
|
||||
gl.linkProgram(prog);
|
||||
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(prog));
|
||||
gl.useProgram(prog);
|
||||
for (const n of ['uSize', 'uOrigin', 'uLightSize', 'uGlowSize', 'uView', 'uLightMap', 'uGlowK', 'uTime', 'uSurf', 'uRain']) SM.u[n] = gl.getUniformLocation(prog, n);
|
||||
// texture unit for each sampler; linear ones are sampled between grains
|
||||
const texs = [['tCol', false, false], ['tColL', true, false], ['tBack', true, false], ['tTree', false, false], ['tField', true, true], ['tLight', true, false], ['tGlow', true, false]];
|
||||
texs.forEach(([name, linear, mip], unit) => {
|
||||
const t = gl.createTexture();
|
||||
gl.activeTexture(gl.TEXTURE0 + unit); gl.bindTexture(gl.TEXTURE_2D, t);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, mip ? gl.LINEAR_MIPMAP_LINEAR : linear ? gl.LINEAR : gl.NEAREST);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, linear ? gl.LINEAR : gl.NEAREST);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||
gl.uniform1i(gl.getUniformLocation(prog, name), unit);
|
||||
SM.tex[name] = { t, unit, mip, w: 0, h: 0 };
|
||||
});
|
||||
gl.bindVertexArray(gl.createVertexArray());
|
||||
SM.canvas = cv; SM.gl = gl; SM.prog = prog; SM.ok = true;
|
||||
SM.white = new Uint8Array([255, 255, 255, 255]);
|
||||
cv.addEventListener('webglcontextlost', e => { e.preventDefault(); SM.ok = false; });
|
||||
} catch (e) {
|
||||
console.warn('Smooth look unavailable, using pixels:', e && e.message);
|
||||
SM.ok = false;
|
||||
}
|
||||
return SM.ok;
|
||||
}
|
||||
function smUpload(name, w, h, data) {
|
||||
const gl = SM.gl, T = SM.tex[name];
|
||||
gl.activeTexture(gl.TEXTURE0 + T.unit); gl.bindTexture(gl.TEXTURE_2D, T.t);
|
||||
if (T.w !== w || T.h !== h) { gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, w, h, 0, gl.RGBA, gl.UNSIGNED_BYTE, data); T.w = w; T.h = h; }
|
||||
else gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, data);
|
||||
if (T.mip) gl.generateMipmap(gl.TEXTURE_2D);
|
||||
}
|
||||
const smBytes = a => new Uint8Array(a.buffer, a.byteOffset, a.byteLength);
|
||||
function smoothDraw(lighting, glowOn) {
|
||||
const t0 = performance.now();
|
||||
// the GPU pass can run below full resolution on a slow graphics card; it is scaled up smoothly
|
||||
const gl = SM.gl, cv = SM.canvas, sc = SM.scale, cw = Math.max(1, Math.round(R.canvas.width * sc)), ch = Math.max(1, Math.round(R.canvas.height * sc)), u = SM.u;
|
||||
if (cv.width !== cw || cv.height !== ch) { cv.width = cw; cv.height = ch; }
|
||||
if (SM.surf < 0 || SM.surfSeed !== worldSeed || SM.surfW !== W) { let s = 0; for (let x = 0; x < W; x++) s += surfY[x]; SM.surf = s / W; SM.surfW = W; SM.surfSeed = worldSeed; }
|
||||
gl.viewport(0, 0, cw, ch);
|
||||
gl.useProgram(SM.prog);
|
||||
const bw = R.bw, bh = R.bh;
|
||||
smUpload('tCol', bw, bh, smBytes(R.px));
|
||||
smUpload('tColL', bw, bh, smBytes(R.px));
|
||||
smUpload('tBack', bw, bh, smBytes(R.backPx));
|
||||
smUpload('tTree', bw, bh, smBytes(R.treePx));
|
||||
smUpload('tField', bw, bh, smBytes(R.fieldPx));
|
||||
if (lighting && R.lightImg) smUpload('tLight', R.lw, R.lh, smBytes(R.lightImg.data)); else smUpload('tLight', 1, 1, SM.white);
|
||||
if (glowOn) smUpload('tGlow', R.glowC.width, R.glowC.height, smBytes(R.glowImg.data)); else smUpload('tGlow', 1, 1, SM.white);
|
||||
gl.uniform2f(u.uSize, bw, bh);
|
||||
gl.uniform2f(u.uOrigin, R.vx0, R.vy0);
|
||||
gl.uniform4f(u.uView, R.camX - R.vx0, R.camY - R.vy0, R.zp * (ch / R.canvas.height), ch);
|
||||
gl.uniform4f(u.uLightMap, lighting ? R.lx0 * R.lres - R.vx0 : 0, lighting ? R.ly0 * R.lres - R.vy0 : 0, R.lres || 1, lighting ? 1 : 0);
|
||||
gl.uniform2f(u.uLightSize, R.lw || 1, R.lh || 1);
|
||||
gl.uniform2f(u.uGlowSize, R.glowC.width || 1, R.glowC.height || 1);
|
||||
gl.uniform1f(u.uGlowK, glowOn ? (SET.glow === 'high' ? 0.9 : 0.55) : 0);
|
||||
gl.uniform1f(u.uTime, (performance.now() - SM.t0) / 1000);
|
||||
gl.uniform1f(u.uSurf, SM.surf);
|
||||
gl.uniform1f(u.uRain, R.rain || 0);
|
||||
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
||||
const ctx = R.ctx;
|
||||
ctx.imageSmoothingEnabled = true;
|
||||
ctx.drawImage(cv, 0, 0, cw, ch, 0, 0, R.canvas.width, R.canvas.height);
|
||||
smoothPace(performance.now() - t0);
|
||||
}
|
||||
// Keep the frame rate up: drop the GPU pass's resolution when it is slow, raise it again when there
|
||||
// is room, and if even half resolution is too slow, give up on the smooth look for this session.
|
||||
function smoothPace(ms) {
|
||||
SM.ms = SM.n ? SM.ms * 0.8 + ms * 0.2 : ms;
|
||||
if (++SM.n < 15) return;
|
||||
SM.n = 1;
|
||||
if (SM.ms > 40 && SM.scale <= 0.5) { SM.ok = false; SM.why = 'slow'; return; }
|
||||
if (SM.ms > 18 && SM.scale > 0.5) SM.scale = Math.max(0.5, SM.scale - (SM.ms > 60 ? 0.5 : 0.25));
|
||||
else if (SM.ms < 6 && SM.scale < 1) SM.scale = Math.min(1, SM.scale + 0.25);
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
// drawWorld: the per-grain pixel buffer (pixel look), also the input of the smooth look.
|
||||
// ---------- World pixels ----------
|
||||
// smooth-look class of a screw tube cell by direction: 9 up, 10 right, 11 down, 12 left
|
||||
const TUBE_CLASS = new Uint8Array(9); TUBE_CLASS[1] = 9; TUBE_CLASS[4] = 10; TUBE_CLASS[2] = 11; TUBE_CLASS[3] = 12;
|
||||
function drawWorld() {
|
||||
const bw = R.bw, bh = R.bh, px = R.px;
|
||||
R.vx0 = Math.floor(R.camX); R.vy0 = Math.floor(R.camY);
|
||||
const vx0 = R.vx0, vy0 = R.vy0;
|
||||
// how hard it is raining around the camera, eased so the sky greys over gently
|
||||
R.rain = (R.rain || 0) * 0.98 + rainAt(Math.max(0, Math.min(W - 1, Math.floor(R.camX + bw / 2)))) * 0.02;
|
||||
const rk = R.rain;
|
||||
// Smooth look: the colours below are still worked out per grain, but each grain also gets a
|
||||
// class (alpha byte) and the GPU draws rounded shapes from them (see smooth.js)
|
||||
const sm = R.smoothOn = SET.look === 'smooth' && !R.lens && typeof smoothAvailable === 'function' && smoothAvailable();
|
||||
const bpx = R.backPx, tpx = R.treePx, fpx = R.fieldPx, mmask = R.moldMask;
|
||||
if (sm) {
|
||||
mmask.fill(0);
|
||||
for (const mo of molds) {
|
||||
if (mo.x + 40 < vx0 || mo.x - 40 > vx0 + bw || mo.y + 40 < vy0 || mo.y - 40 > vy0 + bh) continue;
|
||||
for (const i of moldCellsAt(mo.kind, mo.x, mo.y).walls) { const sx = (i % W) - vx0, sy = ((i / W) | 0) - vy0; if (sx >= 0 && sy >= 0 && sx < bw && sy < bh) mmask[sy * bw + sx] = 1; }
|
||||
}
|
||||
}
|
||||
const OUT = sm ? 0x040a0a0c : 0xff0a0a0c;
|
||||
const lighting = SET.lighting !== 'off' && !R.lens;
|
||||
if (lighting && (R.frame % SET.lightEvery === 0 || !R.lw)) computeLight();
|
||||
const grain = SET.grain, incand = SET.incandescence;
|
||||
const anim = R.frame;
|
||||
const lens = R.lens, tl = THERM[SET.thermalPalette] || THERM.inferno;
|
||||
const glowOn = SET.glow !== 'off' && !lens;
|
||||
const gpx = R.glowPx, gw = R.glowC.width;
|
||||
if (glowOn) gpx.fill(0);
|
||||
for (let by = 0; by < bh; by++) {
|
||||
const y = vy0 + by;
|
||||
const row = by * bw;
|
||||
if (y < 0 || y >= H) { for (let bx = 0; bx < bw; bx++) { px[row + bx] = OUT; if (sm) { bpx[row + bx] = 0x000a0a0c; tpx[row + bx] = 0; fpx[row + bx] = 0xff; } } continue; }
|
||||
const yw = y * W;
|
||||
let skr = SKY_R[y], skg = SKY_G[y], skb = SKY_B[y];
|
||||
if (rk > 0.01) { const gy = (skr + skg + skb) / 3 * 0.82; skr += (gy - skr) * rk * 0.7; skg += (gy - skg) * rk * 0.7; skb += (gy - skb) * rk * 0.65; }
|
||||
for (let bx = 0; bx < bw; bx++) {
|
||||
const x = vx0 + bx;
|
||||
if (x < 0 || x >= W) { px[row + bx] = OUT; if (sm) { bpx[row + bx] = 0x000a0a0c; tpx[row + bx] = 0; fpx[row + bx] = 0xff; } continue; }
|
||||
const i = yw + x, m = mat[i], nz = noiseB[i] - 128;
|
||||
const T = temp[i];
|
||||
let r, g, b;
|
||||
if (lens) {
|
||||
const ti = thermIdx(T);
|
||||
if (m === EMPTY) { r = tl[ti] * 0.55; g = tl[ti + 1] * 0.55; b = tl[ti + 2] * 0.55; }
|
||||
else { const v = (COLR[m] + COLG[m] + COLB[m]) / 3 * 0.25; r = tl[ti] * 0.78 + v; g = tl[ti + 1] * 0.78 + v; b = tl[ti + 2] * 0.78 + v; }
|
||||
const p = part[i];
|
||||
if (p && ((x + y) & 1)) { r = r * 0.7 + 40; g = g * 0.7 + 40; b = b * 0.7 + 40; }
|
||||
px[row + bx] = 0xff000000 | ((b > 255 ? 255 : b) << 16) | ((g > 255 ? 255 : g) << 8) | (r > 255 ? 255 : r);
|
||||
continue;
|
||||
}
|
||||
// background
|
||||
let br, bgc, bb, sky = false;
|
||||
const tb = bgm[i];
|
||||
if (tb && !sm) { const v = nz * CVAR[tb] / 128; br = TREE_R[tb] + v; bgc = TREE_G[tb] + v; bb = TREE_B[tb] + v; if (digp[i]) { const f = 1 + digp[i] / 700; br *= f; bgc *= f; bb *= f; } }
|
||||
else if (y < surfY[x]) { br = skr; bgc = skg; bb = skb; sky = true; }
|
||||
else {
|
||||
const L = y < dirtY[x] ? BG.dirt : y < graniteY[x] ? BG.stone : y < basaltY[x] ? BG.granite : BG.basalt;
|
||||
const v = sm ? 0 : nz * 0.06;
|
||||
br = L[0] + v; bgc = L[1] + v; bb = L[2] + v;
|
||||
}
|
||||
if (sm) {
|
||||
// the colour behind the grain (alpha marks open sky), and a tree in front of that if there is one
|
||||
bpx[row + bx] = (sky ? 0xff000000 : 0) | ((bb & 255) << 16) | ((bgc & 255) << 8) | (br & 255);
|
||||
if (tb) {
|
||||
const v = nz * CVAR[tb] / 256, f = digp[i] ? 1 + digp[i] / 700 : 1;
|
||||
const tr = Math.max(0, Math.min(255, (TREE_R[tb] + v) * f)), tg = Math.max(0, Math.min(255, (TREE_G[tb] + v) * f)), tbb = Math.max(0, Math.min(255, (TREE_B[tb] + v) * f));
|
||||
tpx[row + bx] = ((tb === LEAVES ? 0xff : 0x80) << 24) | (tbb << 16) | (tg << 8) | tr;
|
||||
} else tpx[row + bx] = 0;
|
||||
}
|
||||
if (m === EMPTY) { r = br; g = bgc; b = bb; }
|
||||
else {
|
||||
const v = nz * CVAR[m] * (sm ? (PH[m] === PH_SOLID ? 0 : grain * 0.35) : grain) / 128;
|
||||
r = COLR[m] + v; g = COLG[m] + v; b = COLB[m] + v;
|
||||
if (m === FIRE) { const f = (life[i] & 15) * 6; r = 255; g = 120 + f; b = 30 + f * 0.5; }
|
||||
else if (m === WATER && !sm) { const s = ((x * 3 + y * 5 + (anim >> 2)) & 15) === 0 ? 14 : 0; r += s; g += s; b += s; }
|
||||
else if (m === BELTL || m === BELTR) {
|
||||
const dir = m === BELTR ? -1 : 1, on = netSat(i) > 0, ly = y & 3, lx = x & 3;
|
||||
if (ly === 0 || ly === 3) {
|
||||
// top run moves with the load, bottom run returns the other way
|
||||
const st = ((x + (ly === 0 ? dir : -dir) * (on ? anim >> 1 : 0)) >> 1) & 1;
|
||||
const k = ly === 0 ? 1 : 0.72;
|
||||
r = (st ? 92 : 56) * k; g = (st ? 96 : 58) * k; b = (st ? 104 : 66) * k;
|
||||
} else if (lx === 1 || lx === 2) {
|
||||
const spin = on ? ((anim >> 2) + (dir < 0 ? lx : 3 - lx) + ly) & 1 : (lx + ly) & 1;
|
||||
r = spin ? 132 : 104; g = spin ? 120 : 94; b = spin ? 108 : 84; // copper rollers
|
||||
} else { r = 46; g = 48; b = 54; }
|
||||
} else if (m === PIPE) {
|
||||
pipeCellColor(x, y, anim); r = PC[0]; g = PC[1]; b = PC[2];
|
||||
} else if (m === SHAFT) {
|
||||
const lx = x & 3, ly = y & 3;
|
||||
if (lx === 0 || lx === 3 || ly === 0 || ly === 3) { if (((x + y) & 3) === 0) { r -= 22; g -= 20; b -= 16; } }
|
||||
else { r -= 34; g -= 30; b -= 24; }
|
||||
}
|
||||
else if (m === GATE) { if ((x & 1) === 0 && (y & 3) !== 0) { r -= 40; g -= 24; b -= 20; } }
|
||||
else if (m === CUTST) { const lx = x & 3, ly = y & 3; if (ly === 1 || ly === 3 || lx === (ly < 2 ? 3 : 1)) { r -= 34; g -= 33; b -= 30; } else if (ly === 0 || ly === 2) { r += 10; g += 10; b += 9; } }
|
||||
else if (m === WOOD) { if ((y & 3) === 2 || ((x + (y >> 2) * 3) & 7) === 0) { r -= 26; g -= 22; b -= 16; } } // planks
|
||||
else if (m === LWOOD) { if (((x * 3 + y) & 3) === 0) { r -= 30; g -= 26; b -= 18; } } // stringy fibres
|
||||
else if (m === HULL) { if (((x * 7 + y * 3) & 15) === 0) { r += 20; g += 20; b += 22; } }
|
||||
else if (m === SPRING) { if ((nz & 7) === 0) { r = 90; g = 150; b = 220; } }
|
||||
const al = ALPHA[m];
|
||||
if (al < 1) { r = br + (r - br) * al; g = bgc + (g - bgc) * al; b = bb + (b - bb) * al; }
|
||||
const dp = digp[i];
|
||||
if (dp) {
|
||||
// shaken rock pales with dust and shivers before it shatters (the cracks are drawn over it)
|
||||
const k = dp / 255, lift = k * 30 + Math.sin(anim * 1.9 + x * 1.3 + y * 0.7) * 6 * k;
|
||||
r += lift; g += lift; b += lift * 1.08;
|
||||
}
|
||||
// built wall blocks read as tiles: light top-left edge, dark bottom-right edge
|
||||
if (!sm && PH[m] === PH_SOLID && !(flags[i] & F_NAT) && blockP[(y >> 2) * BW + (x >> 2)] === BP_WALL) {
|
||||
const lx = x & 3, ly = y & 3;
|
||||
if (lx === 0 || ly === 0) { r += 16; g += 16; b += 16; }
|
||||
else if (lx === 3 || ly === 3) { r -= 18; g -= 18; b -= 18; }
|
||||
}
|
||||
}
|
||||
// field parts drawn over contents
|
||||
const p = part[i];
|
||||
if (p && sm && (p <= P_TR || p === P_FILTER)) { if (m === EMPTY || PH[m] === PH_GAS) { r = 0; g = 0; b = 0; } else { r = Math.max(1, r); } }
|
||||
else if (p) {
|
||||
if (p <= P_TR) {
|
||||
const on = netSat(i) > 0;
|
||||
const vert = p === P_TU || p === P_TD;
|
||||
const axis = vert ? y : x, across = vert ? x & 3 : y & 3;
|
||||
const dir = (p === P_TU || p === P_TL) ? 1 : -1;
|
||||
if (across === 0 || across === 3) { // copper casing on the two long sides; its load shows through
|
||||
if (m === EMPTY) { r = 150; g = 96; b = 64; } else { r = r * 0.72 + 44; g = g * 0.72 + 26; b = b * 0.72 + 14; }
|
||||
if ((axis & 3) === 0) { r -= 24; g -= 18; b -= 12; }
|
||||
} else {
|
||||
// screw flights: a diagonal stripe that walks along the tube when powered
|
||||
const ch = (((axis + across + dir * (on ? anim >> 1 : 0)) & 3) === 0);
|
||||
if (m === EMPTY) { r = ch ? 120 : 46; g = ch ? 112 : 46; b = ch ? 104 : 52; }
|
||||
else { r = r * 0.75 + (ch ? 34 : 8); g = g * 0.75 + (ch ? 30 : 8); b = b * 0.75 + (ch ? 28 : 10); }
|
||||
}
|
||||
} else if (p === P_FILTER) {
|
||||
const lx = x & 3, ly = y & 3, fb = (y >> 2) * BW + (x >> 2), dir = blockV[fb] & 128 ? -1 : 1;
|
||||
const fc = filterCfg.get(fb), fms = fc ? fc.m : null, fn = fms ? fms.length : 0, inv = fc && fc.inv;
|
||||
if (ly === 0) { // rollers on top, turning the way the load rides; in carry mode, striped with what rides
|
||||
const st = ((x + dir * (anim >> 1)) >> 1) & 1;
|
||||
if (m === EMPTY || st) {
|
||||
if (inv && fn && !st) { const c = fms[((x + dir * (anim >> 1)) >> 2 & 0xffff) % fn]; r = COLR[c] * 0.8; g = COLG[c] * 0.8; b = COLB[c] * 0.8; }
|
||||
else { r = st ? 92 : 56; g = st ? 96 : 58; b = st ? 104 : 66; }
|
||||
}
|
||||
} else if (ly === 3) { // the belt's return run carries on under it, so belts and filters read as one belt
|
||||
const st = ((x - dir * (anim >> 1)) >> 1) & 1;
|
||||
if (m === EMPTY || st) { r = (st ? 92 : 56) * 0.72; g = (st ? 96 : 58) * 0.72; b = (st ? 104 : 66) * 0.72; }
|
||||
} else if ((lx === 0 && !beltLike(fb - 1)) || (lx === 3 && !beltLike(fb + 1))) { if (m === EMPTY || ((x + y) & 1)) { r = 64; g = 66; b = 74; } } // frame only where the line ends
|
||||
else if (m === EMPTY) { // mesh holes, tinted with what falls through them
|
||||
if ((x + y) & 1) { r = 40; g = 42; b = 48; }
|
||||
else if (inv) { r = 58; g = 74; b = 72; } // carry mode: everything else drops
|
||||
else if (fn) { const c = fms[(((x >> 1) + (y >> 1)) & 0xffff) % fn]; r = COLR[c] * 0.75; g = COLG[c] * 0.75; b = COLB[c] * 0.75; }
|
||||
else { r = 24; g = 25; b = 28; }
|
||||
}
|
||||
} else if (p === P_GRATE) {
|
||||
if ((x + y) & 1) { r = 150; g = 78; b = 58; } else if (m === EMPTY) { r *= 0.6; g *= 0.6; b *= 0.6; }
|
||||
} else if (p === P_GOPEN) {
|
||||
if ((x & 3) === 0) { r = r * 0.6 + 50; g = g * 0.6 + 30; b = b * 0.6 + 26; }
|
||||
} else if (p === P_INTAKE) {
|
||||
if (m === EMPTY) { r = 30; g = 32; b = 36; }
|
||||
if ((x & 1) === 0) { r += 50; g += 54; b += 60; }
|
||||
} else if (p === P_PIPE) {
|
||||
if (pipeBodyCell(x, y)) { thinPipeColor(x, y, anim); r = PC[0]; g = PC[1]; b = PC[2]; }
|
||||
}
|
||||
}
|
||||
// incandescence
|
||||
if (T > 450 && m !== EMPTY && incand) {
|
||||
const gi = glowIndex(PH[m] === PH_SOLID ? T + nz * 0.9 : m === MAGMA ? T + Math.sin(x * 0.19 + y * 0.11 + anim * 0.03 + nz * 0.04) * 55 - 30 : T), w = GLOW_W[gi];
|
||||
r = r * (1 - w) + GLOW_R[gi] * w; g = g * (1 - w) + GLOW_G[gi] * w; b = b * (1 - w) + GLOW_B[gi] * w;
|
||||
if (glowOn && w > 0.3 && ((bx | by) & 1) === 0) {
|
||||
const gk = (by >> 1) * gw + (bx >> 1), s = (w - 0.3) * 1.4;
|
||||
gpx[gk] = 0xff000000 | (Math.min(255, GLOW_B[gi] * s) << 16) | (Math.min(255, GLOW_G[gi] * s) << 8) | Math.min(255, GLOW_R[gi] * s);
|
||||
}
|
||||
}
|
||||
if (r < 0) r = 0; if (g < 0) g = 0; if (b < 0) b = 0;
|
||||
let top = 0xff000000;
|
||||
if (sm) {
|
||||
// what kind of thing this grain is drawn as: 0 open, 1 terrain, 2 loose grain, 3 liquid,
|
||||
// 4 built piece (drawn crisp), 5 smoke and flame, 6 built wall block (drawn as a tile)
|
||||
let cl = 0;
|
||||
const pt = p === P_PIPE && !pipeBodyCell(x, y) ? 0 : p; // the open parts of a pipe block show what is in them
|
||||
if (pt >= P_TU && pt <= P_TR) cl = TUBE_CLASS[pt];
|
||||
else if (p === P_FILTER) {
|
||||
// filter: 13 carries left, 14 right; the tree buffer carries its colour tag (first material) and mode
|
||||
const fb = (y >> 2) * BW + (x >> 2), fc = filterCfg.get(fb), fm = fc && fc.m.length ? fc.m[0] : 0;
|
||||
cl = blockV[fb] & 128 ? 14 : 13;
|
||||
tpx[row + bx] = ((fc && fc.inv ? 0x30 : 0x20) << 24) | (fm ? (COLB[fm] << 16) | (COLG[fm] << 8) | COLR[fm] : 0);
|
||||
}
|
||||
else if (m === BELTL || m === BELTR) cl = m === BELTL ? 7 : 8;
|
||||
else if (pt || PARTMAT[m] || mmask[row + bx]) cl = 4;
|
||||
else if (m !== EMPTY) {
|
||||
const ph = PH[m];
|
||||
if (ph === PH_SOLID) cl = (!(flags[i] & F_NAT) && blockP[(y >> 2) * BW + (x >> 2)] === BP_WALL) ? 6 : 1;
|
||||
else if (ph === PH_POWDER) cl = 2;
|
||||
else if (ph === PH_LIQUID) cl = 3;
|
||||
else cl = 5;
|
||||
}
|
||||
top = cl << 24;
|
||||
fpx[row + bx] = (tb ? 0xff000000 : 0) | (cl === 5 ? 0xff0000 : 0) | (cl === 3 ? 0xff00 : 0) | (cl === 1 || cl === 4 || cl === 6 ? 0xff : 0);
|
||||
}
|
||||
px[row + bx] = top | ((b > 255 ? 255 : b) << 16) | ((g > 255 ? 255 : g) << 8) | (r > 255 ? 255 : r);
|
||||
}
|
||||
}
|
||||
// bars and other loose objects
|
||||
if (!lens) for (const e of ents) {
|
||||
const ex = e.ix - vx0, ey = e.iy - vy0;
|
||||
if (ex + BAR_W < 0 || ey + BAR_H < 0 || ex >= bw || ey >= bh) continue;
|
||||
const m = e.m;
|
||||
for (let dy = 0; dy < BAR_H; dy++) for (let dx = 0; dx < BAR_W; dx++) {
|
||||
const sx = ex + dx, sy = ey + dy;
|
||||
if (sx < 0 || sy < 0 || sx >= bw || sy >= bh) continue;
|
||||
let r = COLR[m], g = COLG[m], b = COLB[m];
|
||||
const end = dx === 0 || dx === BAR_W - 1;
|
||||
if (dy === 0) { r += end ? 10 : 34; g += end ? 8 : 28; b += end ? 6 : 24; } else { r -= end ? 34 : 18; g -= end ? 30 : 16; b -= end ? 26 : 14; }
|
||||
if (e.T > 450 && incand) { const gi = glowIndex(e.T), w = GLOW_W[gi]; r = r * (1 - w) + GLOW_R[gi] * w; g = g * (1 - w) + GLOW_G[gi] * w; b = b * (1 - w) + GLOW_B[gi] * w; }
|
||||
if (r < 0) r = 0; if (g < 0) g = 0; if (b < 0) b = 0;
|
||||
px[sy * bw + sx] = (sm ? 0x04000000 : 0xff000000) | ((b > 255 ? 255 : b | 0) << 16) | ((g > 255 ? 255 : g | 0) << 8) | (r > 255 ? 255 : r | 0);
|
||||
}
|
||||
}
|
||||
// grains in flight from the vacuum nozzle, a tube or a belt (rain and snow are drawn over the top, see drawRain)
|
||||
if (!lens) for (const f of flyers) {
|
||||
if (f.rain) continue;
|
||||
const sx = Math.floor(f.x) - vx0, sy = Math.floor(f.y) - vy0;
|
||||
if (sx < 0 || sy < 0 || sx >= bw || sy >= bh) continue;
|
||||
const m = f.m, v = ((sx * 7 + sy * 13) & 15) - 8;
|
||||
px[sy * bw + sx] = (sm ? 0x02000000 : 0xff000000) | (Math.max(0, Math.min(255, COLB[m] + v)) << 16) | (Math.max(0, Math.min(255, COLG[m] + v)) << 8) | Math.max(0, Math.min(255, COLR[m] + v));
|
||||
}
|
||||
// blueprint ghosts: blocks waiting to be built
|
||||
if (ghostBlocks.size) {
|
||||
const bx0 = vx0 >> 2, by0 = vy0 >> 2, bx1 = (vx0 + bw) >> 2, by1 = (vy0 + bh) >> 2;
|
||||
const pulse = (R.frame >> 3) & 3;
|
||||
for (const [b, gv] of ghostBlocks) {
|
||||
const gbx = b % BW, gby = (b / BW) | 0;
|
||||
if (gbx < bx0 || gbx > bx1 || gby < by0 || gby > by1) continue;
|
||||
const blocked = ghostBlocked.has(b), code = gv[0], v = gv[1];
|
||||
let tr = 90, tg = 190, tb = 230;
|
||||
if (blocked) { tr = 230; tg = 70; tb = 60; }
|
||||
else if (code === BP_WALL && WALL_OF[v]) { const wm = WALL_OF[v]; tr = (COLR[wm] + 90) / 2; tg = (COLG[wm] + 190) / 2; tb = (COLB[wm] + 230) / 2; }
|
||||
for (let dy = 0; dy < BS; dy++) for (let dx = 0; dx < BS; dx++) {
|
||||
const sx = gbx * BS + dx - vx0, sy = gby * BS + dy - vy0;
|
||||
if (sx < 0 || sy < 0 || sx >= bw || sy >= bh) continue;
|
||||
const k = sy * bw + sx, c = px[k];
|
||||
const edge = dx === 0 || dy === 0 || dx === BS - 1 || dy === BS - 1;
|
||||
const a = edge ? (((dx + dy + pulse) & 3) === 0 ? 0.85 : 0.6) : 0.28;
|
||||
const pr = c & 255, pg = (c >> 8) & 255, pb = (c >> 16) & 255;
|
||||
px[k] = (sm ? 0x04000000 : 0xff000000) | ((pb + (tb - pb) * a) << 16) | ((pg + (tg - pg) * a) << 8) | (pr + (tr - pr) * a);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (sm) { smoothDraw(lighting, glowOn); return; }
|
||||
R.bctx.putImageData(R.img, 0, 0);
|
||||
const ctx = R.ctx, zp = R.zp;
|
||||
ctx.imageSmoothingEnabled = false;
|
||||
const ox = -(R.camX - vx0) * zp, oy = -(R.camY - vy0) * zp;
|
||||
ctx.drawImage(R.buf, 0, 0, bw, bh, Math.round(ox), Math.round(oy), bw * zp, bh * zp);
|
||||
if (lighting) {
|
||||
const res = R.lres;
|
||||
ctx.save();
|
||||
ctx.globalCompositeOperation = 'multiply';
|
||||
ctx.imageSmoothingEnabled = true;
|
||||
const lx = (R.lx0 * res - R.camX) * zp, ly = (R.ly0 * res - R.camY) * zp;
|
||||
ctx.drawImage(R.lightC, 0, 0, R.lw, R.lh, lx, ly, R.lw * res * zp, R.lh * res * zp);
|
||||
ctx.restore();
|
||||
}
|
||||
if (glowOn) {
|
||||
R.glowX.putImageData(R.glowImg, 0, 0);
|
||||
ctx.save();
|
||||
ctx.globalCompositeOperation = 'lighter';
|
||||
ctx.globalAlpha = SET.glow === 'high' ? 0.9 : 0.5;
|
||||
ctx.imageSmoothingEnabled = true;
|
||||
const blur = Math.max(2, zp * (SET.glow === 'high' ? 3.2 : 2.2));
|
||||
try { ctx.filter = `blur(${blur.toFixed(1)}px)`; } catch (e) { /* no filter support */ }
|
||||
ctx.drawImage(R.glowC, 0, 0, R.glowC.width, R.glowC.height, Math.round(ox), Math.round(oy), R.glowC.width * 2 * zp, R.glowC.height * 2 * zp);
|
||||
ctx.filter = 'none';
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user