/** * Wall columns, collapsed into runs. * * A raycaster produces one vertical slice per screen column, and the naive * renderer pays one `fillRect` for each of them — a couple of thousand fills * a frame on a wide canvas, most of them identical to their neighbour. The * shading is quantised into levels already (`scene.js`), so a flat wall face * is a stretch of columns asking for exactly the same rectangle, drawn one * pixel at a time. * * This walks the columns once and hands back the runs: the stretches where * the level, the face, the haze over it and the geometry all agree, so the * caller draws one rectangle for the stretch. A corridor collapses hundreds * of fills into a handful and the picture does not change. * * Pure data in, pure data out — no canvas here — and nothing is allocated * per frame: the column arrays and the run objects are kept and rewritten. */ /** A level of -1 is sky: no wall was struck, and no run covers it. */ export const SKY = -1; /** How far apart two slices may sit and still be drawn as one rectangle. */ export const GEOM = 0.5; /** Haze is quantised this finely before it joins the run's identity. */ export const HAZE_STEPS = 16; /** The columns of one frame: what the ray loop writes, all of it flat. */ export function makeColumns(count) { return { count, level: new Int16Array(count).fill(SKY), side: new Int8Array(count), top: new Float32Array(count), high: new Float32Array(count), haze: new Float32Array(count), }; } const run = () => ({ from: 0, to: 0, level: SKY, side: 0, top: 0, high: 0, step: 0, haze: 0 }); /** The quantised haze of a column: equal steps draw as one rectangle. */ export const hazeStep = (haze) => Math.round(Math.min(1, Math.max(0, haze)) * HAZE_STEPS); /** * Write the runs of `cols` into `out`, growing it as needed, and return how * many of its entries are this frame's. `out[i].from` and `.to` are column * indices, both inclusive. */ export function wallRuns(cols, out) { let used = 0; let open = false; // is out[used - 1] still growing, or did sky end it? for (let i = 0; i < cols.count; i++) { const level = cols.level[i]; if (level === SKY) { open = false; continue; } const step = hazeStep(cols.haze[i]); const top = cols.top[i]; const high = cols.high[i]; const last = used > 0 ? out[used - 1] : null; if ( open && last.level === level && last.side === cols.side[i] && last.step === step && Math.abs(last.top - top) <= GEOM && Math.abs(last.high - high) <= GEOM ) { last.to = i; continue; } if (out.length <= used) out.push(run()); const fresh = out[used]; fresh.from = i; fresh.to = i; fresh.level = level; fresh.side = cols.side[i]; fresh.top = top; fresh.high = high; fresh.step = step; fresh.haze = step / HAZE_STEPS; used++; open = true; } return used; } /** How many columns the runs cover: the picture's width, for a test to check. */ export function covered(out, used) { let total = 0; for (let i = 0; i < used; i++) total += out[i].to - out[i].from + 1; return total; }