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:
1 parent
d0c9ea4f0d
commit
b34a9159a7
4 files changed
+57
-4
No files matched your search
+11
@@ -388,6 +388,17 @@ zombie (now an occasional opponent, `TUNE.racketEvery`, not the every-game fixtu
|
|||||||
`assets/_sheet_crowd1.png` is the pre-brightening original of the FIRST crowd, kept for history.
|
`assets/_sheet_crowd1.png` is the pre-brightening original of the FIRST crowd, kept for history.
|
||||||
|
|
||||||
## Settled feel decisions — do not regress silently
|
## Settled feel decisions — do not regress silently
|
||||||
|
- **The frame is dark at BOTH ends.** The run-off apron already took the bottom of the screen down
|
||||||
|
to near-black below the near baseline; `TUNE.standTop` does the same at the top, so the stand fades
|
||||||
|
UP out of the dark instead of being cut off at y = 0. Oliver, 2026-08-21: "the bottom of the screen
|
||||||
|
falls off into a black tone.. please make the top come out of a black tone." It also gives the
|
||||||
|
scoreboard something to sit on.
|
||||||
|
- **Every HUD element on top of the picture carries a drop shadow** (`render.hudShadow`, and the same
|
||||||
|
numbers in CSS on the quit chip): the board, the game/sets panel, the BLOOD panel and each rack
|
||||||
|
tile. They sit on a crowd, and half the overlay lifting off the backdrop while the other half does
|
||||||
|
not is worse than neither. Canvas shadow offsets are NOT scaled by the transform — they are device
|
||||||
|
pixels — so `hudShadow` multiplies by `RES`, or a 3 px shadow reads as three quarters of a logical
|
||||||
|
pixel on a 4x phone. It is sticky state: turn it off the moment the element is drawn.
|
||||||
- **Far bodies draw off a flattened scale curve; the camera holds still.** `TUNE.farK` 0.25
|
- **Far bodies draw off a flattened scale curve; the camera holds still.** `TUNE.farK` 0.25
|
||||||
(render.js `farB`): the court projects true, bodies grow toward the far baseline (~20% at the
|
(render.js `farB`): the court projects true, bodies grow toward the far baseline (~20% at the
|
||||||
back), anchored so the near player never changes. Oliver: *"new perspective is really good —
|
back), anchored so the near player never changes. Oliver: *"new perspective is really good —
|
||||||
|
|||||||
+5
-2
@@ -99,7 +99,10 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
|
|||||||
#quit button { width: calc(var(--hud, 8px) * 4); height: calc(var(--hud, 8px) * 4);
|
#quit button { width: calc(var(--hud, 8px) * 4); height: calc(var(--hud, 8px) * 4);
|
||||||
border-radius: 3px; display: grid; place-items: center;
|
border-radius: 3px; display: grid; place-items: center;
|
||||||
background: rgba(8,26,18,0.82); border: 1px solid rgba(63,149,96,0.35);
|
background: rgba(8,26,18,0.82); border: 1px solid rgba(63,149,96,0.35);
|
||||||
box-shadow: inset 0 1px 0 rgba(242,234,214,0.10), 0 1px 2px rgba(0,0,0,0.45);
|
/* the same drop shadow the canvas HUD carries (render.hudShadow): the chip sits on
|
||||||
|
the crowd like the scoreboard does, and half the overlay lifting off the
|
||||||
|
backdrop while the other half does not is worse than neither. */
|
||||||
|
box-shadow: inset 0 1px 0 rgba(242,234,214,0.10), 3px 3px 3px rgba(0,0,0,0.68);
|
||||||
/* a plain X, not ✕: Silkscreen has no U+2715, so the dingbat fell back to the
|
/* a plain X, not ✕: Silkscreen has no U+2715, so the dingbat fell back to the
|
||||||
system font and drew a smooth glyph inside a pixel HUD */
|
system font and drew a smooth glyph inside a pixel HUD */
|
||||||
color: var(--cream); font: 700 14px Silkscreen, monospace; line-height: 1;
|
color: var(--cream); font: 700 14px Silkscreen, monospace; line-height: 1;
|
||||||
@@ -661,6 +664,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script type="module" src="src/main.js?v=301"></script>
|
<script type="module" src="src/main.js?v=302"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -305,6 +305,10 @@ export const TUNE = {
|
|||||||
// the night court) replaces it rather than adding to it — stacking the two is what made those two
|
// the night court) replaces it rather than adding to it — stacking the two is what made those two
|
||||||
// unreadable: "it is too dim to see anything on the centre court and the dark court".
|
// unreadable: "it is too dim to see anything on the centre court and the dark court".
|
||||||
standBack: 0.22,
|
standBack: 0.22,
|
||||||
|
// ...and how dark the TOP of the frame is before the stand fades up out of it — the mirror of the
|
||||||
|
// run-off apron that already takes the bottom of the screen down to black. Oliver: "the bottom of
|
||||||
|
// the screen falls off into a black tone.. please make the top come out of a black tone."
|
||||||
|
standTop: 0.78,
|
||||||
// ---- damage. Bodies with hp (racket boss 'r', brute 'g') take damage in these units, so a big
|
// ---- damage. Bodies with hp (racket boss 'r', brute 'g') take damage in these units, so a big
|
||||||
// boss is just a bigger hp number and every weapon stays proportional. Fractions are legal: they
|
// boss is just a bigger hp number and every weapon stays proportional. Fractions are legal: they
|
||||||
// bank on zb.chip and only whole points come off hp (the wound SKINS switch on hp thresholds).
|
// bank on zb.chip and only whole points come off hp (the wound SKINS switch on hp thresholds).
|
||||||
|
|||||||
+37
-2
@@ -509,6 +509,19 @@ function paintBackdrop(c, pal, pt) {
|
|||||||
// went wrong".
|
// went wrong".
|
||||||
const dim = standDim(pt);
|
const dim = standDim(pt);
|
||||||
if (dim > 0) { c.fillStyle = `rgba(12,8,20,${dim})`; c.fillRect(0, 0, W, standsBottom); }
|
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
|
// ground + court (Super Tennis lawn); everything outside the doubles court is OUT
|
||||||
const dw = COURT.dblW / 2;
|
const dw = COURT.dblW / 2;
|
||||||
quadXZ(-14, -6, 14, 30, pal.out); // out area. NEVER let z approach
|
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 (fill) { ctx.fillStyle = fill; ctx.fill(); }
|
||||||
if (stroke) { ctx.strokeStyle = stroke; ctx.stroke(); }
|
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) {
|
function drawShadow(px, py, rx, ry, alpha = 0.28) {
|
||||||
ctx.fillStyle = `rgba(0,0,0,${alpha})`;
|
ctx.fillStyle = `rgba(0,0,0,${alpha})`;
|
||||||
ctx.beginPath(); ctx.ellipse(px, py, rx, Math.max(1, ry), 0, 0, 7); ctx.fill();
|
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.
|
// different coordinate systems and only the CSS one is fixed.
|
||||||
const chip = Math.round(44 * W / (cv.clientWidth || innerWidth));
|
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;
|
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)');
|
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.textAlign = 'center'; ctx.textBaseline = 'middle';
|
||||||
ctx.font = F(7, 0); ctx.fillStyle = hp <= 2 ? UI.bad : '#89a08f';
|
ctx.font = F(7, 0); ctx.fillStyle = hp <= 2 ? UI.bad : '#89a08f';
|
||||||
ctx.fillText('BLOOD', x + w / 2, y + 7);
|
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"):
|
// 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.
|
// a visibly brighter well and a 2px gold frame, not the same hairline every other tile gets.
|
||||||
if (sel) ctx.lineWidth = 2;
|
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,
|
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 ? '#1c5230' : '#0a2418', // sunk slot — the live one sits visibly lit
|
||||||
sel ? UI.pick : '#05160e'); // gold frame when live
|
sel ? UI.pick : '#05160e'); // gold frame when live
|
||||||
|
hudShadow(false);
|
||||||
if (sel) ctx.lineWidth = 1;
|
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 —
|
// 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
|
// 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 bW = 150, bH = 42; // fixed: keeps the ornate PNG undistorted (art is 300x84 = 150x42)
|
||||||
const plate = zBoard || SPR.scoreboard;
|
const plate = zBoard || SPR.scoreboard;
|
||||||
if (benchOn) { /* an art test has no score — not even the empty plate */ }
|
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 if (plate) {
|
||||||
else { // code-drawn fallback until the PNG loads
|
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);
|
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, 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(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);
|
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
|
// 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
|
// strings are 7 and the panel grew to carry them — they are the only permanent readout of how
|
||||||
// far into a run you are.
|
// 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));
|
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
|
// 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.
|
// 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;
|
ctx.textAlign = 'right'; ctx.font = F(7, 0); ctx.fillStyle = bd.dim;
|
||||||
|
|||||||
Reference in new issue
Block a user