A real hand for the ghost, and the countdown hugs its frame

- 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 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Fable 5 committed 2026-08-08 20:28:24 +02:00
1 parent aaae452338
commit 9476f354c2
3 files changed
+20 -13

No files matched your search

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

+1 -1
View File
@@ -359,6 +359,6 @@
</div>
</div>
</div>
<script type="module" src="main.js?v=129"></script>
<script type="module" src="main.js?v=130"></script>
</body>
</html>
+19 -12
View File
@@ -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 <img> 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));
}