Overlays paint around a body, not merely behind it

The per-column sprite gate blanked an overlay's WHOLE column wherever a
nearer body touched it — a treasure chest masked the warp veil all the
way to the ceiling. Each column now remembers its nearest sprite's
vertical span as well as its depth, and every overlay family (veils,
ghost walls, lintels, rising conjurations) draws in segments above and
below the covered band, sampling the matching slice of its texture — so
the shimmer wraps the chest instead of vanishing over it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-23 21:53:20 -04:00
co-authored by Claude Fable 5
parent b235a7a9cd
commit be7c451437
+57 -27
View File
@@ -363,10 +363,22 @@
if (s) sprites.push({ ...s, alpha: f.kind === "impact" ? 1 - p : 1, fallback: f.art });
}
sprites.sort((a, b) => b.sort - a.sort);
// The nearest sprite each column carries: the overlay passes (veils,
// ghosts, lintels, risings) must not paint over a body standing in
// front of them.
// The nearest sprite each column carries — depth AND vertical span —
// so the overlay passes (veils, ghosts, lintels, risings) can paint
// around a body standing in front of them instead of over it.
const spriteZ = new Float64Array(W).fill(Infinity);
const sprTop = new Float64Array(W);
const sprBot = new Float64Array(W);
/** The parts of an overlay column NOT hidden behind the column's
* nearest sprite: whole, split around it, or nothing. */
const maskedSegs = (col: number, depth: number, top: number, h: number): [number, number][] => {
if (depth < spriteZ[col]!) return [[top, h]];
const segs: [number, number][] = [];
const bottom = top + h;
if (sprTop[col]! > top) segs.push([top, Math.min(h, sprTop[col]! - top)]);
if (sprBot[col]! < bottom) segs.push([sprBot[col]!, bottom - sprBot[col]!]);
return segs;
};
for (const s of sprites) {
const img = imageFor(s.src) ??
(s.fallback
@@ -385,7 +397,11 @@
if (warpIdCol[col] !== s.warpId || s.depth <= warpDistCol[col]!) continue;
} else if (s.depth >= warpDistCol[col]!) continue;
if (s.clampL !== undefined && (col < s.clampL || col > s.clampR!)) continue;
if (s.depth < spriteZ[col]!) spriteZ[col] = s.depth;
if (s.depth < spriteZ[col]!) {
spriteZ[col] = s.depth;
sprTop[col] = s.top;
sprBot[col] = s.bottom;
}
const texX = ((col - s.left) / (s.right - s.left));
if (img) {
ctx.drawImage(
@@ -411,65 +427,79 @@
// translucent, swirling — the doorway between rooms that are not
// neighbors announces itself.
for (const vl of veils) {
if (vl.depth >= spriteZ[vl.col]!) continue;
const vh = Math.min(H * 2.5, H / Math.max(vl.depth, 0.05));
const vTop = half - vh / 2;
const wt = textures.warp!;
ctx.globalAlpha = 0.3;
ctx.drawImage(wt, Math.min(wt.width - 1, vl.u * wt.width), 0,
Math.max(1, wt.width / 96), wt.height, vl.col, vTop, 1, vh);
ctx.globalAlpha = 1;
const swirl = 0.1 + 0.1 * Math.sin(time / 240 + vl.u * 9 + vl.col * 0.02);
for (const [segTop, segH] of maskedSegs(vl.col, vl.depth, vTop, vh)) {
ctx.globalAlpha = 0.3;
ctx.drawImage(wt, Math.min(wt.width - 1, vl.u * wt.width),
((segTop - vTop) / vh) * wt.height,
Math.max(1, wt.width / 96), (segH / vh) * wt.height,
vl.col, segTop, 1, segH);
ctx.globalAlpha = 1;
ctx.fillStyle = `rgba(190,150,255,${Math.max(0, swirl)})`;
ctx.fillRect(vl.col, vTop, 1, vh);
ctx.fillRect(vl.col, segTop, 1, segH);
}
}
// Conjurations rising: the wall (or stone) grows bottom-up where it
// will stand, glowing swirls running over the young face.
for (const ri of risings) {
if (ri.depth >= spriteZ[ri.col]!) continue;
const full = Math.min(H * 2.5, H / Math.max(ri.depth, 0.05));
const top0 = half + full / 2 - full * ri.g;
const grown = full * ri.g;
const top0 = half + full / 2 - grown;
const tex = textures[ri.kind] ?? textures.wall!;
ctx.globalAlpha = 0.55 + 0.45 * ri.g;
ctx.drawImage(tex, Math.min(tex.width - 1, ri.u * tex.width), 0,
Math.max(1, tex.width / 96), tex.height, ri.col, top0, 1, full * ri.g);
ctx.globalAlpha = 1;
const swirl = (0.35 * (1 - ri.g) + 0.08) *
(0.7 + 0.3 * Math.sin(time / 90 + ri.worldU * 21));
for (const [segTop, segH] of maskedSegs(ri.col, ri.depth, top0, grown)) {
ctx.globalAlpha = 0.55 + 0.45 * ri.g;
ctx.drawImage(tex, Math.min(tex.width - 1, ri.u * tex.width),
((segTop - top0) / grown) * tex.height,
Math.max(1, tex.width / 96), (segH / grown) * tex.height,
ri.col, segTop, 1, segH);
ctx.globalAlpha = 1;
ctx.fillStyle = `rgba(190,150,255,${Math.max(0, swirl)})`;
ctx.fillRect(ri.col, top0, 1, full * ri.g);
ctx.fillRect(ri.col, segTop, 1, segH);
}
}
// Doorway lintels: the frame's top rows hung across each opening.
for (const li of lintels) {
if (li.depth >= spriteZ[li.col]!) continue;
const lh = Math.min(H * 2.5, H / Math.max(li.depth, 0.05));
const lTop = half - lh / 2;
const band = lh * 0.16;
const ft = textures.doorframe!;
ctx.drawImage(ft, Math.min(ft.width - 1, li.u * ft.width), 0,
Math.max(1, ft.width / 96), ft.height * 0.16, li.col, lTop, 1, lh * 0.16);
const dark = 1 - Math.min(1, 1.35 / (1 + li.depth * 0.45));
for (const [segTop, segH] of maskedSegs(li.col, li.depth, lTop, band)) {
ctx.drawImage(ft, Math.min(ft.width - 1, li.u * ft.width),
((segTop - lTop) / band) * ft.height * 0.16,
Math.max(1, ft.width / 96), (segH / band) * ft.height * 0.16,
li.col, segTop, 1, segH);
if (dark > 0.02) {
ctx.fillStyle = `rgba(0,0,0,${Math.min(0.92, dark)})`;
ctx.fillRect(li.col, lTop, 1, lh * 0.16);
ctx.fillRect(li.col, segTop, 1, segH);
}
}
}
// The ghosts of walls you know are lies: drawn over everything at
// their columns, thin as breath, rippling.
for (const gh of ghosts) {
if (gh.depth >= spriteZ[gh.col]!) continue;
const gHft = Math.min(H * 2.5, H / Math.max(gh.depth, 0.05));
const gTop = half - gHft / 2;
const tex = textures.wall!;
ctx.globalAlpha = 0.2;
ctx.drawImage(tex, Math.min(tex.width - 1, gh.u * tex.width), 0,
Math.max(1, tex.width / 96), tex.height, gh.col, gTop, 1, gHft);
ctx.globalAlpha = 1;
const ripple = 0.10 + 0.07 * Math.sin(time / 200 + gh.worldU * 11 + gHft * 0.01);
for (const [segTop, segH] of maskedSegs(gh.col, gh.depth, gTop, gHft)) {
ctx.globalAlpha = 0.2;
ctx.drawImage(tex, Math.min(tex.width - 1, gh.u * tex.width),
((segTop - gTop) / gHft) * tex.height,
Math.max(1, tex.width / 96), (segH / gHft) * tex.height,
gh.col, segTop, 1, segH);
ctx.globalAlpha = 1;
ctx.fillStyle = `rgba(190,160,255,${Math.max(0, ripple)})`;
ctx.fillRect(gh.col, gTop, 1, gHft);
ctx.fillRect(gh.col, segTop, 1, segH);
}
}
// A whisper of vignette holds the torchlit mood together.