Files
deepgrain-foundry/src/render/setup.js
T
44r0n7andClaude Opus 5.5 7fa1056e09 Refactor: merge duplicated rules, split the largest files by topic (t21)
- One helper each for wall and mold refunds, tube edges and lanes, pipe text, piece dimensions,
  font lookup, reach circles and surface averaging.
- previews, smooth, notebook, materials, worldgen and selection split into topic files
  (preview-drill, preview-selection, smooth-shaders, notebook-market, notebook-blueprints,
  material-tables, worldgen-helpers, selection-pickers, selection-panels).
- Every suite's output identical apart from timings; seeds make the same worlds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 22:35:12 -04:00

201 lines
11 KiB
JavaScript

/* 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);
const sm = meanSurface();
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 }; }
function renderMonoFont() {
if (!R.monoFont) R.monoFont = getComputedStyle(document.documentElement).getPropertyValue('--font-mono').trim() || 'monospace';
}