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
+62 -32
View File
@@ -363,10 +363,22 @@
if (s) sprites.push({ ...s, alpha: f.kind === "impact" ? 1 - p : 1, fallback: f.art }); if (s) sprites.push({ ...s, alpha: f.kind === "impact" ? 1 - p : 1, fallback: f.art });
} }
sprites.sort((a, b) => b.sort - a.sort); sprites.sort((a, b) => b.sort - a.sort);
// The nearest sprite each column carries: the overlay passes (veils, // The nearest sprite each column carries — depth AND vertical span —
// ghosts, lintels, risings) must not paint over a body standing in // so the overlay passes (veils, ghosts, lintels, risings) can paint
// front of them. // around a body standing in front of them instead of over it.
const spriteZ = new Float64Array(W).fill(Infinity); 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) { for (const s of sprites) {
const img = imageFor(s.src) ?? const img = imageFor(s.src) ??
(s.fallback (s.fallback
@@ -385,7 +397,11 @@
if (warpIdCol[col] !== s.warpId || s.depth <= warpDistCol[col]!) continue; if (warpIdCol[col] !== s.warpId || s.depth <= warpDistCol[col]!) continue;
} else if (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.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)); const texX = ((col - s.left) / (s.right - s.left));
if (img) { if (img) {
ctx.drawImage( ctx.drawImage(
@@ -411,65 +427,79 @@
// translucent, swirling — the doorway between rooms that are not // translucent, swirling — the doorway between rooms that are not
// neighbors announces itself. // neighbors announces itself.
for (const vl of veils) { 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 vh = Math.min(H * 2.5, H / Math.max(vl.depth, 0.05));
const vTop = half - vh / 2; const vTop = half - vh / 2;
const wt = textures.warp!; 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); const swirl = 0.1 + 0.1 * Math.sin(time / 240 + vl.u * 9 + vl.col * 0.02);
ctx.fillStyle = `rgba(190,150,255,${Math.max(0, swirl)})`; for (const [segTop, segH] of maskedSegs(vl.col, vl.depth, vTop, vh)) {
ctx.fillRect(vl.col, vTop, 1, 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, segTop, 1, segH);
}
} }
// Conjurations rising: the wall (or stone) grows bottom-up where it // Conjurations rising: the wall (or stone) grows bottom-up where it
// will stand, glowing swirls running over the young face. // will stand, glowing swirls running over the young face.
for (const ri of risings) { 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 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!; 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) * const swirl = (0.35 * (1 - ri.g) + 0.08) *
(0.7 + 0.3 * Math.sin(time / 90 + ri.worldU * 21)); (0.7 + 0.3 * Math.sin(time / 90 + ri.worldU * 21));
ctx.fillStyle = `rgba(190,150,255,${Math.max(0, swirl)})`; for (const [segTop, segH] of maskedSegs(ri.col, ri.depth, top0, grown)) {
ctx.fillRect(ri.col, top0, 1, full * ri.g); 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, segTop, 1, segH);
}
} }
// Doorway lintels: the frame's top rows hung across each opening. // Doorway lintels: the frame's top rows hung across each opening.
for (const li of lintels) { 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 lh = Math.min(H * 2.5, H / Math.max(li.depth, 0.05));
const lTop = half - lh / 2; const lTop = half - lh / 2;
const band = lh * 0.16;
const ft = textures.doorframe!; 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)); const dark = 1 - Math.min(1, 1.35 / (1 + li.depth * 0.45));
if (dark > 0.02) { for (const [segTop, segH] of maskedSegs(li.col, li.depth, lTop, band)) {
ctx.fillStyle = `rgba(0,0,0,${Math.min(0.92, dark)})`; ctx.drawImage(ft, Math.min(ft.width - 1, li.u * ft.width),
ctx.fillRect(li.col, lTop, 1, lh * 0.16); ((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, segTop, 1, segH);
}
} }
} }
// The ghosts of walls you know are lies: drawn over everything at // The ghosts of walls you know are lies: drawn over everything at
// their columns, thin as breath, rippling. // their columns, thin as breath, rippling.
for (const gh of ghosts) { 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 gHft = Math.min(H * 2.5, H / Math.max(gh.depth, 0.05));
const gTop = half - gHft / 2; const gTop = half - gHft / 2;
const tex = textures.wall!; 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); const ripple = 0.10 + 0.07 * Math.sin(time / 200 + gh.worldU * 11 + gHft * 0.01);
ctx.fillStyle = `rgba(190,160,255,${Math.max(0, ripple)})`; for (const [segTop, segH] of maskedSegs(gh.col, gh.depth, gTop, gHft)) {
ctx.fillRect(gh.col, gTop, 1, 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, segTop, 1, segH);
}
} }
// A whisper of vignette holds the torchlit mood together. // A whisper of vignette holds the torchlit mood together.