The HUD lifts off the crowd, and the frame is dark at both ends

A drop shadow under the scoreboard, the game/sets panel, the BLOOD panel and
every rack tile — and the same numbers in CSS on the quit chip, because half the
overlay lifting off the backdrop while the other half sits flat on it is worse
than neither. Canvas shadow offsets are device pixels and are not scaled by the
transform, so hudShadow multiplies by RES; without that a 3px shadow is three
quarters of a logical pixel on a 4x phone.

And the top of the frame now fades out of black the way the bottom already fades
into it (TUNE.standTop) — Oliver: "the bottom of the screen falls off into a
black tone.. please make the top come out of a black tone". The stand comes up
out of the dark instead of being cut off at the top edge, which also gives the
scoreboard something to sit on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-21 14:21:12 +02:00
1 parent d0c9ea4f0d
commit b34a9159a7
4 files changed
+57 -4

No files matched your search

+37 -2
View File
@@ -509,6 +509,19 @@ function paintBackdrop(c, pal, pt) {
// went wrong".
const dim = standDim(pt);
if (dim > 0) { c.fillStyle = `rgba(12,8,20,${dim})`; c.fillRect(0, 0, W, standsBottom); }
// THE PICTURE COMES OUT OF THE DARK AT THE TOP, the way it falls into it at the bottom. Same
// colour and the same idea as the run-off apron below the near baseline (further down this
// function): the stand is the far edge of the frame, and a hard cut at y = 0 reads as a
// photograph that has been cropped rather than as a stadium with a roof somewhere above it.
// It also happens to be where the scoreboard sits, which is the second reason it earns its keep.
if (TUNE.standTop > 0) {
const deep = standsBottom * 0.42;
const g = c.createLinearGradient(0, -8, 0, deep);
g.addColorStop(0, `rgba(6,10,6,${TUNE.standTop})`);
g.addColorStop(1, 'rgba(6,10,6,0)');
c.fillStyle = g;
c.fillRect(-8, -8, W + 16, deep + 8);
}
// ground + court (Super Tennis lawn); everything outside the doubles court is OUT
const dw = COURT.dblW / 2;
quadXZ(-14, -6, 14, 30, pal.out); // out area. NEVER let z approach
@@ -617,6 +630,17 @@ function panel(x, y, w, h, r, fill, stroke) {
if (fill) { ctx.fillStyle = fill; ctx.fill(); }
if (stroke) { ctx.strokeStyle = stroke; ctx.stroke(); }
}
// A DROP SHADOW UNDER THE HUD. The board, the blood panel and the rack sit on a crowd now, and an
// overlay lifts off a busy backdrop with a shadow — the same thing shadowText already does for loose
// text, one pixel at a time. Canvas shadow offsets are NOT scaled by the transform (they are device
// pixels), so they are multiplied by RES here, or a 2 px shadow reads as half a logical pixel on a
// 4x phone. Call hudShadow(false) as soon as the element is drawn: the state is sticky and would
// otherwise land under every glyph that follows.
function hudShadow(on, off = 3, blur = 3, a = 0.68) {
ctx.shadowColor = on ? `rgba(0,0,0,${a})` : 'transparent';
ctx.shadowBlur = on ? blur * RES : 0;
ctx.shadowOffsetX = ctx.shadowOffsetY = on ? off * RES : 0;
}
function drawShadow(px, py, rx, ry, alpha = 0.28) {
ctx.fillStyle = `rgba(0,0,0,${alpha})`;
ctx.beginPath(); ctx.ellipse(px, py, rx, Math.max(1, ry), 0, 0, 7); ctx.fill();
@@ -1183,7 +1207,9 @@ function drawBlood() {
// different coordinate systems and only the CSS one is fixed.
const chip = Math.round(44 * W / (cv.clientWidth || innerWidth));
const w = Math.max(46, pw + 10), h = 24, x = W - 6 - chip - w, y = 6;
hudShadow(true);
panel(x + 0.5, y + 0.5, w - 1, h - 1, 3, 'rgba(26,6,8,0.82)', 'rgba(163,74,66,0.45)');
hudShadow(false);
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.font = F(7, 0); ctx.fillStyle = hp <= 2 ? UI.bad : '#89a08f';
ctx.fillText('BLOOD', x + w / 2, y + 7);
@@ -1902,9 +1928,11 @@ function drawRack() {
// The LIVE one has to read at a glance ("the active weapon should be more pronounced"):
// a visibly brighter well and a 2px gold frame, not the same hairline every other tile gets.
if (sel) ctx.lineWidth = 2;
hudShadow(true, 2, 0, 0.5); // hard, no blur: this runs once per slot per frame
panel(x + 1.5, y0 + 1.5, g.tw - 3, g.h - 3, 4,
sel ? '#1c5230' : '#0a2418', // sunk slot — the live one sits visibly lit
sel ? UI.pick : '#05160e'); // gold frame when live
hudShadow(false);
if (sel) ctx.lineWidth = 1;
// THE BURST COUNTDOWN IS THE TILE ITSELF. It was a 1.5 px bar along the tile's bottom edge —
// which is both the rounded frame AND the very bottom of a phone screen, so it was invisible
@@ -2706,9 +2734,14 @@ export function render() {
const bW = 150, bH = 42; // fixed: keeps the ornate PNG undistorted (art is 300x84 = 150x42)
const plate = zBoard || SPR.scoreboard;
if (benchOn) { /* an art test has no score — not even the empty plate */ }
else if (plate) ctx.drawImage(plate, 0, 0, bW, bH); // generated ornate board backdrop
else { // code-drawn fallback until the PNG loads
else if (plate) {
hudShadow(true);
ctx.drawImage(plate, 0, 0, bW, bH); // generated ornate board backdrop
hudShadow(false);
} else { // code-drawn fallback until the PNG loads
hudShadow(true);
R(0, 0, bW, bH, bd.face);
hudShadow(false);
R(0, 0, bW, 1, bd.hi); R(0, 0, 1, bH, bd.hi);
R(0, bH - 1, bW, 1, bd.lo); R(bW - 1, 0, 1, bH, bd.lo);
R(1, 1, 5, bH - 2, bd.spine); R(1, 1, 5, 1, bd.spineHi); R(6, 1, 1, bH - 2, bd.lo);
@@ -2747,7 +2780,9 @@ export function render() {
// 6px was the smallest type in the game and it read as texture rather than as words. Both
// strings are 7 and the panel grew to carry them — they are the only permanent readout of how
// far into a run you are.
hudShadow(true);
panel(0.5, bH + 1.5, bW - 1, 14, 3, 'rgba(8,26,18,0.82)', hexA(bd.hi, 0.35));
hudShadow(false);
// which game you're in, and the sets banked, permanently — the 1.1s GAME N flash and the
// scoreboard's own per-row sets column were the only places either showed.
ctx.textAlign = 'right'; ctx.font = F(7, 0); ctx.fillStyle = bd.dim;