diff --git a/assets/x_hand.png b/assets/x_hand.png new file mode 100644 index 0000000..0997bc5 Binary files /dev/null and b/assets/x_hand.png differ diff --git a/index.html b/index.html index cb40c76..5ecad3d 100644 --- a/index.html +++ b/index.html @@ -359,6 +359,6 @@ - + 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)); }