Every message knows its place now: over the crowd
- one definite band for cards, flash and the countdown — never on the court - the card heading down a size; RESUMING IN finally on a readable panel - the gesture-teaching strings are gone, the ghost hand is the teacher - ghost hand v2: bigger, bigger type, and knuckles that make it a hand Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a89e8ff419
commit
aaae452338
3 files changed
+78
-44
No files matched your search
@@ -1172,25 +1172,31 @@ function drawRack() {
|
||||
|
||||
// the centered milestone 'block': a real pause, front and center. Shared by the sudden-death duel
|
||||
// and the SET announcement so both read as the same interruption. Assumes textAlign is already set.
|
||||
// ---- THE MESSAGE BAND. Every transient centred message — the milestone card, the RESUMING
|
||||
// countdown, the one-line flash (FLASH_Y already lived here) — has ONE definite home: over the
|
||||
// CROWD, below the scoreboard block, above the ad strip. Nothing transient sits on the court
|
||||
// anymore — a card at centre court covered the very play it was announcing, non-blocking or not.
|
||||
const AD_TOP = () => Math.round(proj(0, 0, -6).y) - AD_H; // top of the courtside ad strip
|
||||
const bandTop = h => Math.max(56, AD_TOP() - h - 4); // fit an h-px band above the ads
|
||||
|
||||
function drawCard(title, sub1, sub2, color, icon) {
|
||||
// 80 tall, not 72: the second sub line ends at H/2+26 and the band used to stop at +30, so it sat
|
||||
// all but touching the edge. The band now clears it by 10 px, the same air it has at the top.
|
||||
// An ICON (the won-duel tin) needs its own room above the title, so the band grows for it rather
|
||||
// than the art crowding the type.
|
||||
// An ICON (the won-duel tin) needs its own room above the title, so the band grows for it
|
||||
// rather than the art crowding the type.
|
||||
const img = icon && SPR[icon], ih = img ? 34 : 0;
|
||||
ctx.fillStyle = 'rgba(10,6,8,0.66)'; ctx.fillRect(0, H / 2 - 44 - ih, W, 80 + ih);
|
||||
const y0 = bandTop(68 + ih);
|
||||
ctx.fillStyle = 'rgba(10,6,8,0.72)'; ctx.fillRect(0, y0, W, 68 + ih);
|
||||
ctx.textAlign = 'center';
|
||||
if (img) { // the tin itself, at its own aspect, sat on the title
|
||||
const w = Math.round(img.width * (ih - 6) / img.height);
|
||||
ctx.imageSmoothingEnabled = false;
|
||||
ctx.drawImage(img, Math.round(W / 2 - w / 2), Math.round(H / 2 - 44 - ih + 3), w, ih - 6);
|
||||
ctx.drawImage(img, Math.round(W / 2 - w / 2), y0 + 3, w, ih - 6);
|
||||
}
|
||||
// the title SHOUTS (Bangers, twice the old size); the subs stay Silkscreen, because they are the
|
||||
// detail you read second and a whole card in the loud face is just noise
|
||||
ctx.font = B(30); shadowText(title, W / 2, H / 2 - 34, color);
|
||||
// the title still SHOUTS (Bangers) but at 22, not 30 — up here it shares the screen with the
|
||||
// scoreboard and the ads, and 30 read as oversized the moment it left centre court
|
||||
ctx.font = B(22); shadowText(title, W / 2, y0 + ih + 8, color);
|
||||
ctx.font = F(8);
|
||||
if (sub1) shadowText(sub1, W / 2, H / 2 + 4, UI.cream);
|
||||
if (sub2) shadowText(sub2, W / 2, H / 2 + 18, UI.cream);
|
||||
if (sub1) shadowText(sub1, W / 2, y0 + ih + 36, UI.cream);
|
||||
if (sub2) shadowText(sub2, W / 2, y0 + ih + 50, UI.cream);
|
||||
}
|
||||
|
||||
// A GHOST HAND, fingertip at (x, y) — the tutorial finger every gesture hint shares. A drawn
|
||||
@@ -1198,24 +1204,30 @@ function drawCard(title, sub1, sub2, color, icon) {
|
||||
// finger goes here". `press` (0..1) dips it toward the glass, which is what makes a mimed tap
|
||||
// read as a tap and not as a cursor hovering. Pure canvas paths, no sprite: it is HUD chrome in
|
||||
// the UI palette, same family as the arrowheads and the dotted track, not a body on the court.
|
||||
function drawGhostHand(x, y, press = 0) {
|
||||
function drawGhostHand(x, y, press = 0, s = 1.4) {
|
||||
ctx.save();
|
||||
ctx.translate(Math.round(x), Math.round(y) - 2 + press * 3);
|
||||
ctx.rotate(-0.18); // the slight tilt a right hand actually lands with
|
||||
ctx.fillStyle = UI.cream; ctx.strokeStyle = 'rgba(8,12,10,0.75)'; ctx.lineWidth = 1;
|
||||
ctx.beginPath(); ctx.roundRect(-2.2, 0, 4.4, 11, 2.2); ctx.fill(); ctx.stroke(); // index finger, tip at origin
|
||||
ctx.beginPath(); ctx.roundRect(-3.2, 8, 11.5, 10, 4); ctx.fill(); ctx.stroke(); // the curled fist behind it
|
||||
ctx.beginPath(); ctx.ellipse(-2.6, 12.5, 2.4, 3.6, 0.55, 0, 7); ctx.fill(); ctx.stroke(); // folded thumb
|
||||
ctx.strokeStyle = 'rgba(8,12,10,0.35)'; // two knuckle creases — what makes the blob read as fingers
|
||||
ctx.beginPath(); ctx.moveTo(2.4, 9.5); ctx.lineTo(2.4, 13.5); ctx.moveTo(5.4, 9.5); ctx.lineTo(5.4, 13.5); ctx.stroke();
|
||||
ctx.translate(Math.round(x), Math.round(y) - 3 + press * 4);
|
||||
ctx.scale(s, s); // 1.4 default — the first cut was "you could recognize it, but…" small
|
||||
ctx.rotate(-0.15); // the slight tilt a right hand actually lands with
|
||||
ctx.fillStyle = UI.cream; ctx.strokeStyle = 'rgba(8,12,10,0.8)'; ctx.lineWidth = 1;
|
||||
ctx.beginPath(); ctx.roundRect(-2.5, 0, 5, 15, 2.5); ctx.fill(); ctx.stroke(); // index finger, tip at origin
|
||||
ctx.beginPath(); ctx.roundRect(-2.5, 12, 13, 11, 5); ctx.fill(); ctx.stroke(); // the palm behind it
|
||||
// three CURLED fingers along the palm's leading edge — the knuckle bumps are what make the
|
||||
// silhouette read as a pointing hand instead of a mitten
|
||||
for (const [bx, by, br] of [[4.5, 13.2, 2.4], [7.4, 13.7, 2.2], [9.9, 14.4, 2.0]]) {
|
||||
ctx.beginPath(); ctx.arc(bx, by, br, 0, 7); ctx.fill(); ctx.stroke();
|
||||
}
|
||||
ctx.beginPath(); ctx.ellipse(-3.9, 17.5, 2.6, 4.4, 0.6, 0, 7); ctx.fill(); ctx.stroke(); // thumb, out to the side
|
||||
ctx.strokeStyle = 'rgba(8,12,10,0.35)'; // fingernail hint on the tip
|
||||
ctx.beginPath(); ctx.arc(0, 3, 1.6, 0, 7); ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
// the contact ring a mimed tap leaves on the glass: grows and fades under the fingertip
|
||||
function drawTapRing(x, y, p) {
|
||||
ctx.save();
|
||||
ctx.globalAlpha *= Math.max(0, 0.9 * (1 - p));
|
||||
ctx.strokeStyle = UI.warm; ctx.lineWidth = 1.5;
|
||||
ctx.beginPath(); ctx.arc(x, y, 3 + p * 12, 0, 7); ctx.stroke();
|
||||
ctx.strokeStyle = UI.warm; ctx.lineWidth = 2;
|
||||
ctx.beginPath(); ctx.arc(x, y, 4 + p * 16, 0, 7); ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
@@ -1255,12 +1267,13 @@ function demoSwipe(dx, dy, label) {
|
||||
ctx.lineTo(hx - dx * s * 0.7 - dy * s, hy - dy * s * 0.7 - dx * s);
|
||||
ctx.fill();
|
||||
// ...and say it in words. The mime alone was too abstract to read as an instruction.
|
||||
ctx.font = F(8);
|
||||
ctx.font = F(10); // hint type is 10 now, one size up — arm's-length readable was the ask
|
||||
ctx.textAlign = 'center'; ctx.textBaseline = 'top';
|
||||
// The label sits under the TRACK, not under the arrowhead — following the arrow sideways would
|
||||
// carry it off the edge of the screen, and clearing the full travel below a horizontal stroke
|
||||
// left it floating in the middle of an empty apron with nothing to attach itself to.
|
||||
shadowText(label, ax, ay + (dy ? travel + 24 : 18), UI.warm);
|
||||
// (offsets clear the bigger hand: its palm hangs ~32 px below the fingertip now)
|
||||
shadowText(label, ax, ay + (dy ? travel + 36 : 38), UI.warm);
|
||||
// the ghost hand riding the stroke, pressed into the glass for the whole draw
|
||||
ctx.globalAlpha = gone ? 0 : 0.9;
|
||||
drawGhostHand(ax + dx * len, ay + dy * len, 1);
|
||||
@@ -1294,9 +1307,9 @@ function drawTapHint() {
|
||||
ctx.save(); ctx.globalAlpha = 0.9;
|
||||
if (t >= 0.72 && t < 1.32) drawTapRing(ax, ay, (t - 0.72) / 0.6);
|
||||
drawGhostHand(ax, ay, press);
|
||||
ctx.font = F(8); ctx.textAlign = 'center'; ctx.textBaseline = 'top';
|
||||
shadowText('TAP TO TOSS', ax - 20, ay + 26, UI.warm);
|
||||
shadowText('TAP AGAIN AT THE TOP', ax - 20, ay + 38, UI.warm);
|
||||
ctx.font = F(10); ctx.textAlign = 'center'; ctx.textBaseline = 'top';
|
||||
shadowText('TAP TO TOSS', ax - 20, ay + 40, UI.warm);
|
||||
shadowText('TAP AGAIN AT THE TOP', ax - 20, ay + 54, UI.warm);
|
||||
ctx.restore();
|
||||
}
|
||||
// two dropped tosses in a row: the hand rides the ball itself and presses exactly at the apex,
|
||||
@@ -1308,8 +1321,8 @@ function drawApexHint() {
|
||||
ctx.save(); ctx.globalAlpha = 0.9;
|
||||
if (press > 0.55) drawTapRing(b.x + 4, b.y + 4, 1 - press); // tight at the apex, blooming as it passes
|
||||
drawGhostHand(b.x + 4, b.y + 4, press);
|
||||
ctx.font = F(8); ctx.textAlign = 'center'; ctx.textBaseline = 'top';
|
||||
shadowText('TAP AT THE TOP', b.x, b.y - 24, UI.warm);
|
||||
ctx.font = F(10); ctx.textAlign = 'center'; ctx.textBaseline = 'top';
|
||||
shadowText('TAP AT THE TOP', b.x, b.y - 28, UI.warm);
|
||||
ctx.restore();
|
||||
}
|
||||
// two returns passed untouched: the hand presses harder as the ball closes on the player
|
||||
@@ -1321,8 +1334,8 @@ function drawHitHint() {
|
||||
ctx.save(); ctx.globalAlpha = 0.9;
|
||||
if (press >= 1) drawTapRing(hx, hy, (G.idleT * 2) % 1);
|
||||
drawGhostHand(hx, hy, press);
|
||||
ctx.font = F(8); ctx.textAlign = 'center'; ctx.textBaseline = 'top';
|
||||
shadowText('TAP AS IT ARRIVES', hx - 26, hy + 34, UI.warm);
|
||||
ctx.font = F(10); ctx.textAlign = 'center'; ctx.textBaseline = 'top';
|
||||
shadowText('TAP AS IT ARRIVES', hx - 26, hy + 44, UI.warm);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
@@ -1739,15 +1752,19 @@ export function render() {
|
||||
: Math.max(G.changeover > 0 ? G.changeover : 0,
|
||||
G.phase === 'over' ? G.overT : 0,
|
||||
G.interlude ? G.interlude.introT : 0);
|
||||
if (waitT > 0.5) {
|
||||
// Kept clear of the ad strip — centred on H alone it landed right on the boards, which is the
|
||||
// busiest band on the screen. The strip's own projected line is the ceiling, so it stays legible
|
||||
// whatever the phone's height does to the layout. A 1px shadow, not a box, carries it over the crowd.
|
||||
// A live card owns the band and IS the announcement of the pause — the countdown yields to it.
|
||||
if (waitT > 0.5 && !(G.card && G.card.t > 0 && !(G.card.wait > 0))) {
|
||||
// In the message band with the rest of the family, and on its OWN PANEL: bare shadowed text
|
||||
// was barely readable over the sunlit clay crowd (Oliver flagged it every court since).
|
||||
// Plain Silkscreen on purpose — a countdown is information, not an announcement.
|
||||
const txt = 'RESUMING IN ' + Math.ceil(waitT);
|
||||
const y = Math.min(Math.round(H * 0.34), Math.round(proj(0, 0, -6).y) - AD_H - 16);
|
||||
const y = Math.min(FLASH_Y + 26, AD_TOP() - 18);
|
||||
ctx.font = F(10);
|
||||
shadowText(txt, W / 2, y, hexA(UI.warm, 0.92));
|
||||
const tw = Math.ceil(ctx.measureText(txt).width);
|
||||
ctx.lineWidth = 1;
|
||||
panel(W / 2 - tw / 2 - 7, y - 4, tw + 14, 18, 3, 'rgba(8,26,18,0.85)', hexA(UI.warm, 0.4));
|
||||
ctx.textBaseline = 'top';
|
||||
shadowText(txt, W / 2, y, hexA(UI.warm, 0.95));
|
||||
}
|
||||
if (G.phase === 'pretoss' && score.server === 'P' && G.mode !== 'eaten' && !G.horde && !(G.mgT > 0) && !G.aim && waitT <= 0.5) { // while pulling, the draw meter owns this slot
|
||||
ctx.font = F(8);
|
||||
@@ -1763,15 +1780,15 @@ export function render() {
|
||||
// so this slot shuts up rather than printing the same instruction a second time 40 px below it
|
||||
const demo = G.mode === 'splatter' && !G.interlude && !gunLike && G.idleHintT > TUNE.pullHintAfter;
|
||||
const tapDemo = G.mode === 'tennis' && G.serveNum !== 2 && G.idleHintT > TUNE.pullHintAfter;
|
||||
// The gesture-teaching strings ('PULL DOWN TO AIM…', 'TAP TO TOSS…') are GONE — the ghost hand
|
||||
// is the teacher now, and saying the same thing twice in two registers was clutter. This slot
|
||||
// only carries STATE the mime cannot: which weapon the serve fires, and a second serve.
|
||||
const txt = G.mode === 'splatter' && !G.interlude
|
||||
? (gunLike ? (touch ? `TAP TO UNLOAD THE ${sel}` : `SPACE UNLOADS THE ${sel}`)
|
||||
: G.aim ? 'PULL DOWN: FURTHER = HARDER'
|
||||
: slotId(G.weapon) !== 'ball' ? `SERVE FIRES: ${sel} · ${switchHint}`
|
||||
: demo ? ''
|
||||
: touch ? 'PULL DOWN TO AIM · RELEASE TO FIRE' : 'DRAG DOWN TO AIM · SPACE SERVES')
|
||||
: !G.aim && slotId(G.weapon) !== 'ball' ? `SERVE FIRES: ${sel} · ${switchHint}`
|
||||
: '')
|
||||
: G.serveNum === 2 ? (touch ? 'SECOND SERVE — TAP' : 'SECOND SERVE — SPACE')
|
||||
: tapDemo ? '' // the ghost hand says it in mime and in its own words
|
||||
: (touch ? 'TAP TO TOSS, TAP TO SERVE' : 'SPACE TO TOSS, SPACE TO SERVE');
|
||||
: '';
|
||||
if (txt) shadowText(txt, W / 2, METER_Y(), UI.warm); // same slot the draw meter uses
|
||||
if (demo) drawPullHint(); // show the gesture instead of only naming it
|
||||
if (tapDemo) drawTapHint();
|
||||
|
||||
Reference in new issue
Block a user