From 9476f354c214e891b5a3cc9fe6a5218d68d6797b Mon Sep 17 00:00:00 2001 From: Oliver Rieger Date: Sat, 8 Aug 2026 20:28:24 +0200 Subject: [PATCH] A real hand for the ghost, and the countdown hugs its frame MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - x_hand.png generated to Oliver's reference pose (index up, curled knuckles, thumb) — drawGhostHand draws the art, vector stays as fallback - fingertip anchor measured off the PNG, not guessed - RESUMING panel top no longer floats above the glyphs Co-Authored-By: Claude Fable 5 --- assets/x_hand.png | Bin 0 -> 13756 bytes index.html | 2 +- render.js | 31 +++++++++++++++++++------------ 3 files changed, 20 insertions(+), 13 deletions(-) create mode 100644 assets/x_hand.png diff --git a/assets/x_hand.png b/assets/x_hand.png new file mode 100644 index 0000000000000000000000000000000000000000..0997bc5554e110cae15b412f4904d769d25f65ce GIT binary patch literal 13756 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 m%SbDk&N)=67nm@#5c>ZeNP+q-ms&Fb0000 - + diff --git a/render.js b/render.js index 49234e3..80661a3 100644 --- a/render.js +++ b/render.js @@ -187,7 +187,7 @@ export function proj(x, y, z) { // Asset cache-buster — bump with main.js?v=N (see the img.src note below). EXPORTED because the // menu screens hang tags on the same files, and two copies of this number is one copy too // many: the one that does not get bumped is the one that serves a cached 404. -export const SPRV = 86; +export const SPRV = 87; // The boot bar in main.js watches this. Nothing here CHANGES when things load — every sprite was // already requested at import — it just makes the wait visible and holds the menu back until the // last sheet has landed. See the onerror note below for the other half of "sometimes missing". @@ -202,6 +202,7 @@ for (const n of ['p_idle1','p_idle2','p_bounce','p_bounce2','p_toss','p_smash',' 'x_crowd_h','x_crowd_h2','x_ads_h', // THE SLAB (hard) 'x_boymop1','x_boymop2','x_boymop3','x_boymop4', // the push cycle, used only while he is stopped and working 'x_boycarry1','x_boycarry2','x_boycarry3','x_boycarry4','x_howto','x_cross', // walking with the mop shouldered, and the slingshot tutorial panel + 'x_hand', // the ghost tutorial hand (drawGhostHand) — generated art, Oliver's reference pose 'z_gw2','z_gw3','z_gw4','z_gwb','z_gw2b','z_gw3b','z_gw4b', // the brute comes apart in stages (4/5 hp = z_gw .. 1/5 = z_gw4), each with a 2-frame lurch (…b) 'z_boss1','z_boss2','z_bossw1','z_bossw2','z_bossw3', // the CHAPTER BOSS: 2-frame stride + three wound stages (arm off -> ribs out -> headless) 'z_bosst0','z_bosst1','z_bosst2','z_bosst3', // ...and each of those four states mid-THROW (windT) @@ -1204,22 +1205,26 @@ 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, s = 1.4) { +function drawGhostHand(x, y, press = 0, hPx = 34) { + const img = SPR.x_hand, yy = Math.round(y) - 3 + press * 4; + if (img) { // real art now (assets/x_hand.png, drawn to Oliver's reference pose) + const w = Math.round(img.width * hPx / img.height); + ctx.imageSmoothingEnabled = false; + // 0.368: the index fingertip's x as a fraction of the PNG's width, MEASURED off the sprite + // (topmost opaque row's centroid) so the tip lands exactly on the point being taught + ctx.drawImage(img, Math.round(x - w * 0.368), yy, w, hPx); + return; + } + // fallback while the sprite loads (or a cached 404 — this project's oldest trap): the drawn hand ctx.save(); - 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.translate(Math.round(x), yy); ctx.scale(1.4, 1.4); ctx.rotate(-0.15); 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.arc(bx, by, br, 0, 7); ctx.fill(); ctx.stroke(); // curled knuckles } - 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.beginPath(); ctx.ellipse(-3.9, 17.5, 2.6, 4.4, 0.6, 0, 7); ctx.fill(); ctx.stroke(); // thumb ctx.restore(); } // the contact ring a mimed tap leaves on the glass: grows and fades under the fingertip @@ -1762,7 +1767,9 @@ export function render() { ctx.font = F(10); 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)); + // panel top hugs the glyphs: Silkscreen carries its own top leading under baseline 'top', + // so a symmetric-looking box needs the frame to start a px above the text, not four + panel(W / 2 - tw / 2 - 7, y - 1, tw + 14, 15, 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)); }